warmbly

package module
v0.3.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: 24 Imported by: 0

README

warmbly-go

The official Go SDK for the Warmbly cold-outreach & mailbox-warmup platform.

Go Reference Go Report Card CI codecov Go Version Release

warmbly-go is a fully typed client for the Warmbly REST API and its realtime event gateway. It covers the whole customer-facing v1 surface — mailboxes and warmup, campaigns and sequences, contacts and CRM, the unified inbox, integrations and automations, AI generation and the Advisor, analytics, webhooks, keys and billing — plus a persistent gateway connection for live events. It has zero external dependencies: the entire module is built on the Go standard library, including a dependency-free RFC 6455 WebSocket implementation, so adding it pulls in nothing but Warmbly itself.

Features

  • The whole API, typed. Every service hangs off one Client; see Services for the map.
  • Flexible authentication. API keys via warmbly.WithAPIKey, OAuth 2.1 access tokens via warmbly.WithAccessToken / warmbly.WithTokenSource, and session tokens from client.Auth.Login for the routes an API key deliberately cannot reach. Full OAuth client flows: authorization-code with PKCE and client-credentials.
  • Safe retries. Exponential backoff with jitter honoring Retry-After, plus per-request Idempotency-Key support so retrying a send never sends twice.
  • Typed errors. A decoded *warmbly.Error carrying the request ID and message, matchable with errors.Is against sentinels such as warmbly.ErrNotFound and warmbly.ErrRateLimited.
  • Cursor pagination. A generic Page[T] with a Go 1.23 auto-paging iterator.
  • Webhook verification. Signature and replay checking via client.Webhooks.ConstructEvent.
  • Realtime gateway. A persistent connection with intent filtering, typed handlers, heartbeats, reconnection and sequence-based replay of missed events.
  • Forward compatible. client.Do reaches an endpoint this release does not model yet, and WithQueryParam adds a filter that landed after it shipped.
  • Zero dependencies. Standard library only. No transitive supply chain to audit.

Installation

go get github.com/warmbly/warmbly-go

Requires Go 1.23+ (the auto-paging iterator uses range-over-func).

Quickstart

package main

import (
    "context"
    "fmt"
    "log"

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

func main() {
    client, err := warmbly.New(warmbly.WithAPIKey("wmbly_..."))
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()
    page, err := client.Campaigns.List(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }
    for campaign, err := range page.All(ctx) {
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(campaign.Name)
    }
}

Authentication

API keys

The simplest way to authenticate is with a Warmbly API key (they start with the wmbly_ prefix):

client, err := warmbly.New(warmbly.WithAPIKey("wmbly_..."))
if err != nil {
    log.Fatal(err)
}
OAuth 2.1

For applications acting on behalf of users, use the OAuth 2.1 authorization-code flow with PKCE:

cfg := &warmbly.OAuth2Config{
    ClientID:     "...",
    ClientSecret: "...",
    RedirectURL:  "https://app.example.com/callback",
    Scopes:       []string{"campaigns:read", "contacts:read"},
}
verifier := warmbly.GenerateVerifier()
authURL := cfg.AuthCodeURL("state-xyz", warmbly.S256ChallengeOption(verifier))
// redirect the user to authURL; on the callback:
tok, err := cfg.Exchange(ctx, code, warmbly.VerifierOption(verifier))
client, err := cfg.NewClient(ctx, tok)

For machine-to-machine access, use the client-credentials flow via warmbly.ClientCredentialsConfig. You can register, list, and manage your OAuth applications programmatically through client.OAuthApps.

If you already hold an access token, authenticate directly with warmbly.WithAccessToken:

client, err := warmbly.New(warmbly.WithAccessToken("..."))

Transparent refresh. Pass a token source with warmbly.WithTokenSource to have the client fetch and refresh tokens automatically, so requests never fail on an expired access token. The configs returned by the OAuth flows produce clients backed by a refreshing token source out of the box.

Session tokens

Some of the API is deliberately unreachable with a long-lived key: workspace governance, billing, and the AI assistant all act as a named person rather than an integration. client.Auth signs a user in and yields a session token for those.

Sign-in is two steps. The first emails a code; the second exchanges it for tokens. An account with two-factor enabled takes one more, through Auth.VerifyTwoFA.

session, _, err := client.Auth.Login(ctx, &warmbly.LoginParams{Email: email, Password: password})
tokens, _, err := client.Auth.LoginConfirm(ctx, &warmbly.ConfirmParams{Session: session, Code: emailedCode})

authed, err := warmbly.New(warmbly.WithAccessToken(tokens.AccessToken))
org, _, err := authed.Organization.Current(ctx)

An API key used on one of these routes gets a clean warmbly.ErrUnauthorized rather than a confusing failure.

Working with resources

Every resource is exposed as a service on the Client. Listing returns a page that you can iterate with the auto-paging iterator; individual records are fetched by ID, and most resources support creation:

ctx := context.Background()

// List with automatic pagination.
page, err := client.Campaigns.List(ctx, nil)
if err != nil {
    log.Fatal(err)
}
for campaign, err := range page.All(ctx) {
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(campaign.Name)
}

// Fetch a single record by ID (single-record calls also return the *Response).
campaign, _, err := client.Campaigns.Get(ctx, "camp_123")
if err != nil {
    log.Fatal(err)
}

// Create a new record.
created, _, err := client.Campaigns.Create(ctx, &warmbly.CampaignCreateParams{
    Name: "Q3 outbound",
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(created.ID)

Services

Service What it covers
client.Emails Connected mailboxes, warmup lifecycle, domain authentication, address verification, one-off sends
client.Campaigns Campaigns, sequence steps, A/B variants, attachments, senders, preflight, template preview
client.Contacts Contacts and the 360 view, faceted search, address verification, CRM notes, import and export, AI research
client.Segments Saved contact audiences, evaluated live, with per-contact overrides
client.Suppressions The workspace do-not-contact list
client.Forms Hosted lead-capture forms, their submissions and their custom domain
client.Unibox Unified inbox: reading, replying, composing, labels, snoozes, scheduled sends, AI drafts
client.Templates Reply templates, spam scoring, rendering, ordering
client.Analytics Dashboard, per-campaign engagement, warmup progress, deliverability health, plan usage
client.Advisor Continuous checks on sending posture, with one-click and agent fixes
client.CRM Pipelines, deals, task types and the task board
client.Teams Named groups of members for CRM assignment
client.Meetings Calls booked through a connected scheduler
client.Integrations Third-party connections, event subscriptions, field mappings, contact pushes
client.Automations Event-triggered flows across those connections
client.LeadSync Google Sheets to contacts sync
client.Generation AI writing and rewriting
client.Skills Workspace AI playbooks that steer it
client.AgentTools The AI tool registry over plain HTTP, for function-calling agents
client.Webhooks Endpoints, the event catalog, and the delivery log
client.APIKeys Keys, scopes and usage analytics
client.OAuthApps OAuth 2.1 application registration and the consent flow
client.Outreach Organization-wide sending policy
client.Deliverability Bounce and complaint ingestion from an upstream pipeline
client.WarmupRouting Warmup partner-selection rules
client.Tasks Send-task dead-letter queue
client.AuditLogs The organization audit trail
client.Folders / Tags / Categories The label sets that organize campaigns, mailboxes and contacts
client.Meta Caller identity, plan catalog, timezones
client.Auth Sign-in, sessions, profile, two-factor, passkeys, notifications
client.Organization Workspace settings, members, roles, invitations, danger zone
client.Billing Subscription, plan changes, AI credits, referrals
client.WebsiteTracking The website tracking snippet's consent, precision, hosts and retention
client.PoolLink Self-hosted instances linked to this workspace's warmup pool
client.CloudLink A self-hosted instance's own side of that link

Auth, Organization, Billing, WebsiteTracking, PoolLink and CloudLink are session-only; see Session tokens.

Reaching something new

The API moves faster than this SDK's release cadence. client.Do issues a request against any path, with the same authentication, retries and typed errors as a generated method:

var out map[string]any
_, err := client.Do(ctx, http.MethodGet, "some/new/endpoint", nil, &out)

warmbly.WithQueryParam does the same for a filter that a typed parameter struct does not carry yet.

Idempotency

Anything that sends mail or spends money accepts an Idempotency-Key. Retrying with the same key replays the original response instead of acting twice, which turns an ambiguous timeout into a safe retry:

result, resp, err := client.Emails.Send(ctx, mailboxID, params,
    warmbly.WithIdempotencyKey("order-4171-welcome"))
if resp.IdempotentReplayed {
    // The original send already went out; this was a replay.
}

Pagination

List endpoints use cursor-based pagination. Each call returns a page object, and rather than threading cursors through your own loop you can range over page.All(ctx) — a Go 1.23 range-over-func iterator that transparently fetches subsequent pages as you consume items, stopping on the first error:

page, err := client.Emails.List(ctx, nil)
if err != nil {
    log.Fatal(err)
}
for email, err := range page.All(ctx) {
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(email.Email)
}

The iterator yields both a value and an error on each step, so per-page fetch failures surface inline; break out of the loop at any time to stop paging.

Errors

API failures are decoded into a typed *warmbly.Error. Match well-known conditions with errors.Is, or unwrap the full error with errors.As to read details such as the request ID:

if errors.Is(err, warmbly.ErrNotFound) {
    // ...
}
var apiErr *warmbly.Error
if errors.As(err, &apiErr) {
    log.Printf("request %s failed: %s", apiErr.RequestID, apiErr.Message)
}

Key sentinels include warmbly.ErrNotFound, warmbly.ErrUnauthorized, and warmbly.ErrRateLimited.

Sentinels match on the status code, which is often not specific enough to act on: several distinct refusals share a 403. Every error also carries a stable machine-readable code, so branch on that when the remedy differs:

var apiErr *warmbly.Error
if errors.As(err, &apiErr) && apiErr.HasCode(warmbly.ErrCodeMailboxAllowanceReached) {
    // The workspace holds its whole mailbox allowance. Request an increase
    // rather than retrying, which will keep failing.
}

The codes are declared as warmbly.ErrCode* constants.

Retries & rate limits

The client automatically retries transient failures using exponential backoff with jitter, and honours the Retry-After header when the server sends one. Rate-limit headers from each response are parsed and exposed on the returned *Response (resp.RateLimit) so you can observe your remaining quota. Tune retry behaviour with the warmbly.WithMaxRetries option:

client, err := warmbly.New(
    warmbly.WithAPIKey("wmbly_..."),
    warmbly.WithMaxRetries(5),
)

Realtime gateway

The gateway subpackage holds a websocket open to a workspace and delivers typed events as they happen. Declare the intents you want, register handlers with gateway.On, and the client handles heartbeats, reconnection and replay of missed events.

import "github.com/warmbly/warmbly-go/gateway"

g := gateway.New(apiKey, orgID,
    gateway.WithIntents(gateway.IntentCampaign, gateway.IntentEmail))

gateway.On(g, gateway.EventEmailOpened, func(ctx context.Context, e *gateway.EngagementEvent) {
    log.Printf("contact %s opened a message", e.ContactID)
})

if err := g.Open(ctx); err != nil {
    log.Fatal(err)
}
defer g.Close()
<-ctx.Done()

Every event carries a monotonic per-workspace sequence number. The client replays the gap after a reconnect, so a brief drop loses nothing; replay is at-least-once, so deduplicate on Event.Seq if your handler is not idempotent. A disconnect that outlasts the server's buffer surfaces as EventResumeFailed, your cue to resync from the REST API.

A refused channel join surfaces as a *gateway.JoinError carrying the server's code and reason slug. Its Permanent method separates the refusals worth retrying from the ones that will never succeed: a topic you may not see, or an id that does not resolve, is final. A join refused for rate limiting is retried automatically on the same socket once the server's retry_after_ms elapses, so no reconnect is spent on it.

Intents only ever narrow the stream — a credential without unibox access receives no inbox events however it asks. Matching is a substring test against the event type, so an intent must be specific enough not to catch its neighbours. The underlying transport is the dependency-free RFC 6455 implementation in internal/wsconn, so the gateway adds no third-party packages either.

Webhooks

Verify every inbound delivery before trusting it. client.Webhooks.ConstructEvent checks the HMAC-SHA256 signature from the X-Warmbly-Signature header and rejects a stale one, which defeats replay:

event, err := client.Webhooks.ConstructEvent(body, r.Header.Get(warmbly.WebhookSignatureHeader), endpointSecret)
switch {
case errors.Is(err, warmbly.ErrWebhookSignatureExpired):
    http.Error(w, "stale signature", http.StatusUnauthorized)
    return
case err != nil:
    http.Error(w, "invalid signature", http.StatusUnauthorized)
    return
}

A new endpoint receives nothing until it proves it owns its URL. Call client.Webhooks.Verify; Warmbly then sends a signed webhook.test delivery carrying a challenge token, which you echo back in the X-Warmbly-Webhook-Challenge response header. Take the token from the verified payload, not from the copy in the request header — that copy is attacker-controllable, the signed body is not. See examples/webhooks for the full handler.

Deliveries retry, so deduplicate on event.ID.

For lower-level use, warmbly.VerifyWebhookSignature and warmbly.ConstructWebhookEvent expose the same checks with an explicit tolerance.

Examples

The examples/ directory has a runnable program for each part of the SDK — API keys, both OAuth flows, campaigns, contacts, emails/warmup, templates, analytics, webhooks, the real-time gateway, and error handling. See examples/README.md for the full index.

Versioning

warmbly-go follows semantic versioning. While the module is pre-1.0, the public API may change between minor releases; review the release notes before upgrading.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md and our CODE_OF_CONDUCT.md before opening an issue or pull request.

License

Released under the MIT License. See LICENSE for details.

Documentation

Overview

Package warmbly is the official Go SDK for the Warmbly API.

Warmbly is a cold-outreach and mailbox-warmup platform. This package is a typed client for the whole customer-facing v1 surface; the gateway subpackage streams the same workspace's events over a websocket.

Installation

go get github.com/warmbly/warmbly-go@latest

Authentication

Three credentials reach three different slices of the API:

  • API keys (prefixed "wmbly_") for server-to-server access scoped to one workspace. Create a client with WithAPIKey.
  • OAuth 2.1 access tokens (prefixed "wmblyo_") for an application acting for a user. Run the authorization-code or client-credentials flow with OAuth2Config or ClientCredentialsConfig, then pass the token with WithAccessToken or WithTokenSource.
  • Session tokens from AuthService.Login for the routes a long-lived key deliberately cannot reach: workspace governance, billing and the AI assistant. An API key on one of those gets a clean ErrUnauthorized.

Quick start

client, err := warmbly.New(warmbly.WithAPIKey("wmbly_..."))
if err != nil {
	log.Fatal(err)
}

page, err := client.Campaigns.List(ctx, nil)
if err != nil {
	log.Fatal(err)
}
for campaign, err := range page.All(ctx) {
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(campaign.Name)
}

Shape of the API

Every resource group is a service on the Client: Client.Emails, Client.Campaigns, Client.Contacts, Client.Unibox, Client.CRM and the rest. Each method takes a context, then its parameters, then a variadic list of RequestOption.

Methods that return one record return it alongside the Response, so rate-limit state and the request id stay reachable. List methods return a Page that auto-pages through Page.All.

Retrying safely

The client retries 429 and 5xx responses with jittered exponential backoff, honoring Retry-After. That is safe for reads, but a retried send could go out twice — so anything that sends mail or spends money accepts an idempotency key, and repeating it replays the original response:

result, resp, err := client.Emails.Send(ctx, id, params,
	warmbly.WithIdempotencyKey("order-4171-welcome"))
if resp.IdempotentReplayed {
	// The original send already went out.
}

Errors

Every non-2xx response decodes into an Error carrying the message, code and request id. Match it with errors.Is against the package sentinels (ErrNotFound, ErrRateLimited and the rest) rather than comparing status codes by hand.

A sentinel matches on the status alone, which several distinct refusals share. When the remedy differs, branch on the stable code instead, using the ErrCode constants and Error.HasCode:

var apiErr *warmbly.Error
if errors.As(err, &apiErr) && apiErr.HasCode(warmbly.ErrCodeStorageLimitReached) {
	// Free some attachment storage; retrying will not help.
}

Reaching something new

The API ships faster than this SDK. Client.Do issues a request against any path with the same authentication, retries and typed errors, and WithQueryParam adds a filter no typed parameter carries yet.

Design

The module depends only on the standard library, including its own RFC 6455 websocket implementation, so it adds no transitive supply chain.

Index

Examples

Constants

View Source
const (
	AdvisorCritical = "critical"
	AdvisorHigh     = "high"
	AdvisorMedium   = "medium"
	AdvisorLow      = "low"
)

Finding severities returned in AdvisorFinding.Severity.

View Source
const (
	AdvisorCategoryDeliverability = "deliverability"
	AdvisorCategoryMailbox        = "mailbox"
	AdvisorCategoryWarmup         = "warmup"
	AdvisorCategoryCampaign       = "campaign"
	AdvisorCategoryCopy           = "copy"
	AdvisorCategoryList           = "list"
)

Finding categories returned in AdvisorFinding.Category.

View Source
const (
	AdvisorSurfaceCampaigns      = "campaigns"
	AdvisorSurfaceMailboxes      = "emails"
	AdvisorSurfaceDeliverability = "deliverability"
	AdvisorSurfaceContacts       = "contacts"
	AdvisorSurfaceAnalytics      = "analytics"
	AdvisorSurfaceSettings       = "settings"
)

Surfaces a finding is shown on, returned in AdvisorFinding.Surface.

View Source
const (
	AdvisorStatusOpen      = "open"
	AdvisorStatusSnoozed   = "snoozed"
	AdvisorStatusDismissed = "dismissed"
	AdvisorStatusApplied   = "applied"
	AdvisorStatusResolved  = "resolved"
)

Finding states returned in AdvisorFinding.Status.

View Source
const (
	// AIVariableInstant resolves from what is already known about the contact.
	AIVariableInstant = "instant"
	// AIVariableResearch researches the contact first, and costs more.
	AIVariableResearch = "research"
)

AI variable resolution modes for AIVariableParams.Mode.

View Source
const (
	Period7Days  = "7d"
	Period30Days = "30d"
	Period90Days = "90d"
)

Rolling windows accepted by AnalyticsService.Dashboard.

View Source
const (
	UsagePeriodDay   = "day"
	UsagePeriodWeek  = "week"
	UsagePeriodMonth = "month"
)

Usage windows accepted by AnalyticsService.Usage.

View Source
const (
	BandHealthy     = "healthy"
	BandWatch       = "watch"
	BandThrottled   = "throttled"
	BandQuarantined = "quarantined"
	BandBlocked     = "blocked"
)

Deliverability health bands returned in DeliverabilityDashboard.Band and in the per-mailbox and per-campaign breakdowns.

View Source
const (
	// PermReadEmails grants reading mailboxes and their settings.
	PermReadEmails uint64 = 1 << iota
	// PermReadCampaigns grants reading campaigns and their steps.
	PermReadCampaigns
	// PermReadContacts grants reading contacts, segments, notes and
	// activities.
	PermReadContacts
	// PermReadUnibox grants reading the unified inbox.
	PermReadUnibox
	// PermReadAnalytics grants reading analytics and statistics.
	PermReadAnalytics

	// PermWriteEmails grants modifying mailbox settings.
	PermWriteEmails
	// PermWriteCampaigns grants creating and editing campaigns and steps.
	PermWriteCampaigns
	// PermWriteContacts grants creating and editing contacts, segments, notes
	// and activities.
	PermWriteContacts
	// PermWriteUnibox grants marking messages seen and sending replies.
	PermWriteUnibox

	// PermBulkContacts grants bulk contact import, export and delete. It is
	// separate so a key can read and write without bulk power.
	PermBulkContacts
	// PermBulkCampaigns grants bulk campaign operations.
	PermBulkCampaigns

	// PermRealtimeSubscribe grants subscribing to the realtime gateway.
	PermRealtimeSubscribe
	// PermWebhooks grants managing webhook endpoints.
	PermWebhooks

	// PermAPIKeys grants creating, listing and revoking API keys, so an
	// integration can rotate its own credentials.
	PermAPIKeys

	// PermSendCampaigns grants starting and stopping campaigns. It is separate
	// from PermWriteCampaigns because starting one actually sends mail.
	PermSendCampaigns

	// PermReadTemplates grants reading reply templates.
	PermReadTemplates
	// PermWriteTemplates grants creating and editing reply templates.
	PermWriteTemplates
	// PermReadCRM grants reading pipelines, deals and CRM tasks.
	PermReadCRM
	// PermWriteCRM grants creating and editing pipelines, deals and CRM tasks.
	PermWriteCRM

	// PermReadAuditLogs grants reading the organization audit trail.
	PermReadAuditLogs

	// PermIntegrations grants connecting and managing third-party integrations
	// and automations.
	PermIntegrations
	// PermWarmupRouting grants managing warmup routing rules.
	PermWarmupRouting

	// PermAIAgent grants running the AI assistant and the MCP tool surface.
	PermAIAgent
	// PermAIResearch grants running AI contact research.
	PermAIResearch
)

API permission bits. A key's grant is the bitwise OR of the scopes it holds, which is what travels in APIKey.Permissions:

perms := warmbly.PermReadCampaigns | warmbly.PermWriteCampaigns

These are distinct from the organization role permissions that gate a human session.

View Source
const (
	// PermReadOnly grants every read scope and nothing else.
	PermReadOnly = PermReadEmails | PermReadCampaigns | PermReadContacts |
		PermReadUnibox | PermReadAnalytics | PermReadTemplates |
		PermReadCRM | PermReadAuditLogs

	// PermFullAccess grants every scope this SDK release knows about. A key
	// minted with it will not pick up scopes added later.
	PermFullAccess = PermReadOnly | PermWriteEmails | PermWriteCampaigns |
		PermWriteContacts | PermWriteUnibox | PermBulkContacts |
		PermBulkCampaigns | PermSendCampaigns | PermWriteTemplates |
		PermWriteCRM | PermRealtimeSubscribe | PermWebhooks | PermAPIKeys |
		PermIntegrations | PermWarmupRouting | PermAIAgent | PermAIResearch
)

Preset permission masks matching the presets the API advertises.

View Source
const (
	APIKeyStatusActive  = "active"
	APIKeyStatusRevoked = "revoked"
	APIKeyStatusExpired = "expired"
)

API key lifecycle states returned in APIKey.Status.

View Source
const (
	// MinRateLimitPerMinute and MaxRateLimitPerMinute bound an explicit
	// per-key request ceiling; DefaultRateLimitPerMinute is what a key gets
	// when it names none.
	MinRateLimitPerMinute     = 1
	MaxRateLimitPerMinute     = 10000
	DefaultRateLimitPerMinute = 60
	// MaxAllowedIPs is the most entries an IP allow-list may hold, and
	// MaxAllowedEmailAccounts the most mailboxes a key may be pinned to.
	MaxAllowedIPs           = 64
	MaxAllowedEmailAccounts = 128
)

Bounds the server enforces on APIKeyCreateParams and APIKeyUpdateParams.

View Source
const (
	IntervalMinute = "minute"
	IntervalHour   = "hour"
	IntervalDay    = "day"
)

Bucket granularities accepted by APIKeyAnalyticsParams.Interval.

View Source
const (
	// AgentEventTextDelta is an incremental chunk of the reply.
	AgentEventTextDelta = "text_delta"
	// AgentEventText is a complete block of reply text.
	AgentEventText = "text"
	// AgentEventToolStart announces a tool call beginning.
	AgentEventToolStart = "tool_start"
	// AgentEventToolResult carries what a tool returned.
	AgentEventToolResult = "tool_result"
	// AgentEventApprovalRequired pauses the run until
	// [AssistantService.Approve] answers.
	AgentEventApprovalRequired = "approval_required"
	// AgentEventError ends the run with a failure.
	AgentEventError = "error"
	// AgentEventDone ends the run normally.
	AgentEventDone = "done"
)

Event types streamed during a run, in AgentEvent.Type.

View Source
const (
	// ApprovalApprove runs the paused tool call once.
	ApprovalApprove = "approve"
	// ApprovalDeny abandons it and lets the assistant continue without it.
	ApprovalDeny = "deny"
	// ApprovalAlwaysAllow runs it and stops asking for that tool in this
	// session.
	ApprovalAlwaysAllow = "always_allow"
)

Decisions accepted by AssistantService.Approve.

View Source
const (
	AuditActionCreate    = "create"
	AuditActionUpdate    = "update"
	AuditActionDelete    = "delete"
	AuditActionDuplicate = "duplicate"
	AuditActionSend      = "send"

	// Lifecycle actions on a campaign or a mailbox's warmup.
	AuditActionStart  = "start"
	AuditActionStop   = "stop"
	AuditActionPause  = "pause"
	AuditActionResume = "resume"

	// Credentials and connections.
	AuditActionRevoke     = "revoke"
	AuditActionRotate     = "rotate"
	AuditActionRotateKeys = "rotate_keys"
	AuditActionConnect    = "connect"
	AuditActionDisconnect = "disconnect"
	AuditActionTest       = "test"

	// Membership and ownership.
	AuditActionInvite   = "invite"
	AuditActionRemove   = "remove"
	AuditActionAssign   = "assign"
	AuditActionTransfer = "transfer"

	// Bulk movement of data in and out of the workspace.
	AuditActionExport = "export"
	AuditActionImport = "import"

	// AuditActionAPICall records a call made with an API key, when the key is
	// configured to log its requests.
	AuditActionAPICall = "api_call"
	// AuditActionApply records an advisor recommendation being applied.
	AuditActionApply = "apply"
)

Values for AuditLog.Action. The trail is append-only and the vocabulary grows, so treat an action you do not recognize as informational rather than failing on it.

View Source
const (
	AuditEntityCampaign     = "campaign"
	AuditEntityContact      = "contact"
	AuditEntityEmailAccount = "email_account"
	AuditEntityAPIKey       = "api_key"
	AuditEntityWebhook      = "webhook"
	AuditEntityTemplate     = "template"
	// AuditEntitySequence is a step within a campaign's sequence. The wire
	// value is "step".
	AuditEntitySequence     = "step"
	AuditEntityOrganization = "organization"
	AuditEntityUser         = "user"

	// Audiences and the do-not-contact list.
	AuditEntitySegment     = "segment"
	AuditEntityForm        = "form"
	AuditEntitySuppression = "suppression"

	// Labels.
	AuditEntityFolder   = "folder"
	AuditEntityTag      = "tag"
	AuditEntityCategory = "category"

	// Team and access.
	AuditEntityOrganizationMember = "organization_member"
	AuditEntityInvitation         = "invitation"
	AuditEntityRole               = "role"
	AuditEntityTeam               = "team"

	// CRM.
	AuditEntityCRMPipeline = "crm_pipeline"
	AuditEntityCRMStage    = "crm_stage"
	AuditEntityCRMDeal     = "crm_deal"
	AuditEntityCRMTask     = "crm_task"
	AuditEntityCRMNote     = "crm_note"

	// Connections and flows.
	AuditEntityIntegration    = "integration"
	AuditEntityAutomation     = "automation"
	AuditEntityLeadSyncSource = "lead_sync_source"
	AuditEntityMeeting        = "meeting"
	AuditEntityUnibox         = "unibox"

	// Warmup and sending posture.
	AuditEntityWarmupRoutingRule = "warmup_routing_rule"
	// AuditEntityOrgRisk is a change in the workspace's sending posture.
	AuditEntityOrgRisk = "org_risk"

	// AI.
	AuditEntityAISession      = "ai_session"
	AuditEntityAISkill        = "ai_skill"
	AuditEntityMCPServer      = "mcp_server"
	AuditEntityAdvisorFinding = "advisor_finding"

	// Billing.
	AuditEntitySubscription   = "subscription"
	AuditEntityReferral       = "referral"
	AuditEntityReferralCredit = "referral_credit"
	AuditEntityCreditPurchase = "credit_purchase"
	AuditEntityCreditGrant    = "credit_grant"

	// Workspace settings and the archives used to move a workspace between
	// instances.
	AuditEntitySettings   = "settings"
	AuditEntityOrgArchive = "org_archive"

	// The self-hosted warmup pool link, from the cloud side
	// ([AuditEntityPoolLink]) and the instance side ([AuditEntityCloudLink]).
	AuditEntityPoolLink  = "pool_link"
	AuditEntityCloudLink = "cloud_link"
)

Values for AuditLog.EntityType. As with actions, the vocabulary grows; branch on the ones you care about and pass the rest through.

The platform's own entities (workers, releases, instance settings) are audited too, but on the operator's trail rather than any organization's, so they never appear here.

View Source
const (
	// LoginCodeAlways emails a code on every password sign-in.
	LoginCodeAlways = "always"
	// LoginCodeNewDevice emails a code only from a browser the account has
	// not signed in from before, or when the sign-in looks anomalous.
	LoginCodeNewDevice = "new_device"
	// LoginCodeOff never emails a code: [AuthService.Login] returns the
	// tokens directly.
	LoginCodeOff = "off"
)

Login-code policies returned in AuthConfig.LoginCode.

View Source
const (
	// RegistrationOpen means anyone may create an account.
	RegistrationOpen = "true"
	// RegistrationInviteOnly means a signup needs an invitation token
	// ([LoginParams.Invite]).
	RegistrationInviteOnly = "invite_only"
	// RegistrationClosed means signups are off and invitations do not
	// override it.
	RegistrationClosed = "false"
)

Registration policies returned in AuthConfig.Registration.

View Source
const (
	SSOProviderOIDC   = "oidc"
	SSOProviderGoogle = "google"
	SSOProviderApple  = "apple"
)

Browser SSO providers accepted by AuthService.BeginSSO and listed in AuthConfig.Providers.

View Source
const (
	// CLIAuthPending means no member has decided yet.
	CLIAuthPending = "pending"
	// CLIAuthApproved means a member approved; the poll that sees it carries
	// the key.
	CLIAuthApproved = "approved"
	// CLIAuthClaimed means the key has already been handed out and the code
	// is spent.
	CLIAuthClaimed = "claimed"
	// CLIAuthDenied means a member declined.
	CLIAuthDenied = "denied"
)

CLI device-flow handshake states returned in CLIAuthPoll.Status and CLIAuthRequest.Status.

View Source
const (
	NotifInboundReply    = "inbound_reply"
	NotifInboundOOO      = "inbound_out_of_office"
	NotifHealthBounce    = "health_bounce"
	NotifHealthComplaint = "health_complaint"
	NotifWorkerDowntime  = "health_worker_downtime"
	NotifSecuritySignIn  = "security_new_signin"
	NotifBillingAlert    = "billing_alert"
	NotifTeamActivity    = "team_activity"
	// NotifCampaignPaused fires when the platform pauses a campaign on its
	// own, for example when an auto-pause guardrail is breached.
	NotifCampaignPaused = "campaign_paused"
	// NotifDomainAuth fires when a sending domain starts failing SPF or
	// DMARC: the warning before the send gate applies.
	NotifDomainAuth = "health_domain_auth"
)

Notification categories used in Notification.Category and the preference map.

View Source
const (
	// TriggerInboundWebhook runs the automation when an external system POSTs
	// JSON to its [Automation.InboundURL]. It is a trigger only and is never
	// delivered outbound.
	TriggerInboundWebhook = "inbound.webhook"
	// TriggerContactCreated fires for every new contact, whatever created it
	// (import, form, API, another automation). The event carries the contact
	// fields plus source, source_detail, custom_fields, campaign_ids and
	// category_ids, and no campaign.
	TriggerContactCreated = "contact.created"
	// TriggerFormSubmitted fires when a hosted form is submitted. The event
	// carries form_id, form_name, submission_id, source_url and the raw
	// answers under data, alongside the contact fields the answers mapped to.
	TriggerFormSubmitted = "form.submitted"
	// TriggerReplyReceived fires on an inbound campaign reply, with intent
	// and confidence from the classifier.
	TriggerReplyReceived = "campaign.reply_received"
)

Trigger events accepted in Automation.TriggerEvent. Any Event* constant from the webhook catalog can be a trigger; these are the ones with dedicated semantics or no webhook twin.

View Source
const (
	ActionSlackNotify      = "slack.notify"
	ActionDiscordNotify    = "discord.notify"
	ActionHubSpotUpsert    = "hubspot.upsert_contact"
	ActionPipedriveUpsert  = "pipedrive.upsert_person"
	ActionSalesforceUpsert = "salesforce.upsert_contact"
	ActionCloseUpsert      = "close.upsert_lead"
	ActionWebhookPing      = "webhook.ping"

	ActionAddTag        = "warmbly.add_tag"
	ActionRemoveTag     = "warmbly.remove_tag"
	ActionCreateTask    = "warmbly.create_task"
	ActionCreateDeal    = "warmbly.create_deal"
	ActionMoveDealStage = "warmbly.move_deal_stage"
	ActionUnsubscribe   = "warmbly.unsubscribe"
	// ActionLabelEmail applies conversation labels to the thread a reply
	// event belongs to; on triggers without a thread it is a logged no-op.
	ActionLabelEmail = "warmbly.label_email"
	// ActionRunAutomation launches another automation with the current event
	// data, one hop deeper in the chain.
	ActionRunAutomation = "warmbly.run_automation"
	// ActionSetVariables computes named values from templates and writes them
	// into the event data for later nodes.
	ActionSetVariables = "warmbly.set_variables"
	// ActionFireEvent publishes a custom event to the realtime gateway.
	ActionFireEvent = "warmbly.fire_event"
	// ActionUpsertContact creates a contact from templated event fields, or
	// enriches the one already holding that email, then tags it and enrolls
	// it in a campaign. It is the one action that can run before the event has
	// a contact: the written contact becomes the event's contact for every
	// node after it. Configure it with [UpsertContactConfig].
	ActionUpsertContact = "warmbly.upsert_contact"
	// ActionAddToCampaign enrolls the event's contact in a campaign and wakes
	// it. Configure it with [AddToCampaignConfig].
	ActionAddToCampaign = "warmbly.add_to_campaign"
	// ActionAIStep is the unified AI node (classify, extract, generate or a
	// bounded tool-using agent); ActionAISwitch routes to exactly one named
	// case. Both spend AI credits, including on a dry run.
	ActionAIStep   = "warmbly.ai_step"
	ActionAISwitch = "warmbly.ai_switch"
)

Action identifiers for AutomationNode.Action. Provider actions run against the node's ConnectionID; the built-in "warmbly." actions need no connection and run against the contact the event resolved (contact_id or contact_email in the event data).

View Source
const (
	// IfExistsUpdate enriches the existing contact: rendered fields that are
	// empty never erase what the contact already has. The default.
	IfExistsUpdate = "update"
	// IfExistsSkip leaves the existing contact untouched but still makes it
	// the event's contact for the nodes that follow.
	IfExistsSkip = "skip"
)

Policies accepted in UpsertContactConfig.IfExists.

View Source
const (
	// NodeTrigger is the entry point; there is exactly one, conventionally
	// with the id "trigger".
	NodeTrigger = "trigger"
	// NodeCondition branches on the event data.
	NodeCondition = "condition"
	// NodeAction performs work, against a connection or built in.
	NodeAction = "action"
	// NodeStop is a terminal marker: a path routed into it ends. It carries
	// no action or condition.
	NodeStop = "stop"
)

Node kinds in an automation graph.

View Source
const (
	// ConditionField tests one event-data key, named by Key, with Operator
	// and Value.
	ConditionField = "field"
	// ConditionExpression evaluates Expression, a Go-template predicate, and
	// takes the true branch when it renders non-empty and not "false".
	ConditionExpression = "expression"
	// ConditionAI asks the model the yes/no question in Prompt about the
	// event. It costs one AI credit per evaluation.
	ConditionAI = "ai"
	// ConditionRandom is a deterministic percentage split; use
	// [ConditionOpChance] with Value as the percentage.
	ConditionRandom = "random"

	// ConditionIntent tests the reply classifier's intent.
	ConditionIntent = "intent"
	// ConditionConfidence tests the classifier's confidence, a float.
	ConditionConfidence = "confidence"
	// ConditionSource tests the campaign or provider the event came from.
	ConditionSource = "source"
	// ConditionHasContact tests whether the event resolved a contact email.
	ConditionHasContact = "has_contact"
)

Condition kinds accepted in AutomationCondition.Field.

View Source
const (
	ConditionOpEquals    = "equals"
	ConditionOpNotEquals = "not_equals"
	ConditionOpContains  = "contains"
	ConditionOpGte       = "gte"
	ConditionOpLte       = "lte"
	ConditionOpExists    = "exists"
	ConditionOpIsTrue    = "is_true"
	ConditionOpChance    = "chance"
)

Operators accepted in AutomationCondition.Operator.

View Source
const (
	NodeStatusSuccess     = "success"
	NodeStatusError       = "error"
	NodeStatusSkipped     = "skipped"
	NodeStatusBranchTrue  = "branch_true"
	NodeStatusBranchFalse = "branch_false"
)

Per-node outcomes returned in AutomationNodeResult.Status.

View Source
const (
	AutomationRunning = "running"
	AutomationSuccess = "success"
	AutomationError   = "error"
)

Automation run outcomes returned in AutomationRun.Status.

View Source
const (
	EdgeWhenTrue  = "true"
	EdgeWhenFalse = "false"
	// EdgeWhenError is the path taken when the source action fails.
	EdgeWhenError = "error"
	// EdgeCasePrefix prefixes an AI switch case name, as in "label:interested".
	EdgeCasePrefix = "label:"
)

Branch labels for AutomationEdge.When. A condition node's two outgoing edges must be EdgeWhenTrue and EdgeWhenFalse. An action node takes a plain edge (empty When) plus an optional EdgeWhenError branch, and an ActionAISwitch node may additionally carry one EdgeCasePrefix edge per configured case. Anything else is rejected when the automation is saved.

View Source
const (
	ProrationCreate        = "create_prorations"
	ProrationAlwaysInvoice = "always_invoice"
	ProrationNone          = "none"
)

Proration behaviors accepted by ChangePlanParams.ProrationBehavior.

View Source
const (
	// ReferralStatusPending means the invitee signed up but has not converted
	// yet.
	ReferralStatusPending = "pending"
	// ReferralStatusQualified means the conversion counts; the reward is owed.
	ReferralStatusQualified = "qualified"
	// ReferralStatusRewarded means the credit has been applied.
	ReferralStatusRewarded = "rewarded"
	// ReferralStatusVoid means the reward was withdrawn, for example after a
	// refund. See [ReferralAttribution.VoidReason].
	ReferralStatusVoid = "void"
)

Reward states returned in ReferralAttribution.Status.

View Source
const (
	// DiscountTypePercent takes a percentage off.
	DiscountTypePercent = "percent"
	// DiscountTypeFixed takes a fixed amount off.
	DiscountTypeFixed = "fixed"
	// DiscountTypeTrialExtension adds trial days instead of reducing a
	// charge.
	DiscountTypeTrialExtension = "trial_extension"
)

What a discount code grants, in DiscountPreview.Type and DiscountRedemption.Type.

View Source
const (
	DiscountDurationOnce      = "once"
	DiscountDurationRepeating = "repeating"
	DiscountDurationForever   = "forever"
)

How long a money discount keeps applying, in DiscountPreview.Duration.

View Source
const (
	DiscountRedemptionPending  = "pending"
	DiscountRedemptionApplied  = "applied"
	DiscountRedemptionCanceled = "canceled"
)

Redemption states returned in DiscountRedemption.Status.

View Source
const (
	CampaignStatusDraft     = "draft"
	CampaignStatusScheduled = "scheduled"
	CampaignStatusActive    = "active"
	CampaignStatusPaused    = "paused"
	// CampaignStatusPausedNoAccounts is set when the campaign loses every
	// sender, or no sender can send under its settings (a failing SPF/DMARC
	// domain, a sending-behavior profile with no working days).
	CampaignStatusPausedNoAccounts = "paused_no_accounts"
	// CampaignStatusPausedTrialExpired is set when the organization's trial
	// ends while the campaign is running.
	CampaignStatusPausedTrialExpired = "paused_trial_expired"
	// CampaignStatusPausedGuardrail is set when an auto-pause guardrail trips;
	// [Campaign.GuardrailReason] says which one.
	CampaignStatusPausedGuardrail = "paused_guardrail"
	// CampaignStatusPausedUndeliverable is set when address verification has
	// refused every remaining lead. Re-verify them or mark them deliverable
	// (POST /contacts/verification) to resume; starting again from this
	// status is not gated by the list bounce-risk check.
	CampaignStatusPausedUndeliverable = "paused_undeliverable"
	CampaignStatusCompleted           = "completed"
	CampaignStatusStopped             = "stopped"
)

Campaign lifecycle states returned in Campaign.Status.

The server stores draft, active, paused, completed and the paused_* variants. Every paused_* variant is a self-inflicted pause with a specific cause and is restartable with CampaignService.Start once the cause is fixed. "Waiting for leads" is not a status: a continuous campaign that has run out of leads stays active and sets Campaign.IdleSince instead.

View Source
const (
	// CampaignKindSequence is the multi-step default.
	CampaignKindSequence = "sequence"
	// CampaignKindOneTime is a single message with no follow-ups. It is a
	// normal campaign underneath (same pool, caps, suppression and analytics)
	// but accepts at most one email step and refuses further ones.
	CampaignKindOneTime = "one_time"
)

Campaign kinds returned in Campaign.Kind. The kind is fixed at creation.

View Source
const (
	// SenderStrategyTags resolves the sending pool from the mailbox tags in
	// [Campaign.EmailTags].
	SenderStrategyTags = "tags"
	// SenderStrategyExplicit uses the pool managed through
	// [CampaignService.Senders].
	SenderStrategyExplicit = "explicit"
)

Sender-selection strategies for Campaign.SenderStrategy.

View Source
const (
	ESPMatchOff    = "off"
	ESPMatchPrefer = "prefer"
	ESPMatchStrict = "strict"
)

ESP-matching modes for Campaign.ESPMatchMode, which bias sends towards a mailbox on the same provider as the recipient.

View Source
const (
	// ErrCodeListBounceRisk is returned (400) when the list's projected bounce
	// rate is too high: above 4% of known-invalid addresses among deliverable
	// leads, on lists of at least 50. Launch anyway with
	// [CampaignStartParams.AcknowledgeListRisk].
	ErrCodeListBounceRisk = "list_bounce_risk"
	// ErrCodeLeadsUndeliverable is returned (400) when every remaining lead was
	// refused by address verification. The campaign is parked at
	// [CampaignStatusPausedUndeliverable].
	ErrCodeLeadsUndeliverable = "leads_undeliverable"
	// ErrCodeNoLeads is returned (400) when a campaign that has never had a
	// lead is started with [CampaignUpdateParams.Continuous] off. Add contacts,
	// or set Continuous so it starts empty and waits for them. A campaign whose
	// leads have all finished is a different case: it starts and waits, with
	// [CampaignStatusChange.WaitingForLeads] set.
	ErrCodeNoLeads = "no_leads"
	// ErrCodeNoRemainingLeads is returned (400) only to a platform-initiated
	// restart of a campaign with nothing left to send and Continuous off; the
	// campaign returns to [CampaignStatusCompleted]. A start you request never
	// answers this, because it turns Continuous on and waits instead.
	ErrCodeNoRemainingLeads = "no_remaining_leads"
)

Error codes (Error.Code) a CampaignService.Start can refuse with.

View Source
const (
	// StepKindEmail renders and sends the step's subject and body.
	StepKindEmail = "email"
	// StepKindAction runs a side effect described by [Step.Action].
	StepKindAction = "action"
	// StepKindWait delays without sending.
	StepKindWait = "wait"
)

Step kinds returned in Step.Kind.

View Source
const (
	// VerificationSourceProbe is the built-in check (syntax, MX, SMTP probe).
	VerificationSourceProbe = "probe"
	// VerificationSourceProvider is a connected verification service.
	VerificationSourceProvider = "provider"
	// VerificationSourceImported is a verdict that arrived with the contact,
	// through [ContactInput.VerificationStatus] or an import column. The
	// background check leaves it alone until it ages out.
	VerificationSourceImported = "imported"
	// VerificationSourceManual is a verdict a member set through
	// [ContactService.RequestVerification]. It is never re-checked
	// automatically.
	VerificationSourceManual = "manual"
)

Who produced a contact's verification verdict, returned in Contact.VerificationSource. Empty means the address was never checked.

View Source
const (
	VerificationSubStatusCatchAll    = "catch_all"
	VerificationSubStatusDisposable  = "disposable"
	VerificationSubStatusRole        = "role"
	VerificationSubStatusSpamtrap    = "spamtrap"
	VerificationSubStatusMailboxFull = "mailbox_full"
	VerificationSubStatusNoMX        = "no_mx"
	VerificationSubStatusSyntax      = "syntax"
	VerificationSubStatusUndisclosed = "undisclosed"
)

Refinements of a verification status, returned in Contact.VerificationSubStatus. Empty when the status needs no qualifier.

View Source
const (
	// VerificationProviderBuiltin is Warmbly's own check.
	VerificationProviderBuiltin = "builtin"
	// VerificationProviderWarmbly names Warmbly's vocabulary (valid, risky,
	// invalid, unknown) when supplying a verdict you already hold.
	VerificationProviderWarmbly         = "warmbly"
	VerificationProviderMillionVerifier = "millionverifier"
	VerificationProviderZeroBounce      = "zerobounce"
	VerificationProviderNeverBounce     = "neverbounce"
	VerificationProviderBouncer         = "bouncer"
	VerificationProviderKickbox         = "kickbox"
	VerificationProviderEmailable       = "emailable"
	VerificationProviderDebounce        = "debounce"
	VerificationProviderClearout        = "clearout"
	VerificationProviderEmailListVerify = "emaillistverify"
)

Verification vocabularies accepted in ContactInput.VerificationProvider and ImportColumnMapping.VerificationProvider, plus the two verifiers a workspace can run (ContactVerificationOverview.Provider).

View Source
const (
	// LeadStatusPending means the contact is enrolled but nothing has been sent.
	LeadStatusPending = "pending"
	// LeadStatusActive means some but not all steps have been sent.
	LeadStatusActive = "active"
	// LeadStatusCompleted means every step was sent with no reply.
	LeadStatusCompleted = "completed"
	// LeadStatusReplied is terminal: the contact replied.
	LeadStatusReplied = "replied"
	// LeadStatusBounced is terminal: a send hard-bounced.
	LeadStatusBounced = "bounced"
	// LeadStatusFailed is terminal: the mailbox could not send a step after
	// every retry. [ContactCampaignProgress.FailureReason] says why.
	LeadStatusFailed = "failed"
	// LeadStatusUnsubscribed is terminal: the contact is suppressed.
	LeadStatusUnsubscribed = "unsubscribed"
	// LeadStatusUndeliverable is a lead the campaign skips because address
	// verification refused it (invalid, or risky with the campaign's risky
	// toggle off). Re-verifying or marking it deliverable
	// ([ContactService.RequestVerification]) puts it back in the queue.
	LeadStatusUndeliverable = "undeliverable"
)

Derived lead states returned in ContactCampaignProgress.Status, ContactCampaignState.LeadStatus and accepted by ContactSearchParams.LeadStatus. Highest priority first: unsubscribed, bounced, replied, failed, completed, active, undeliverable, pending.

View Source
const (
	LeadEngagementOpened     = "opened"
	LeadEngagementNotOpened  = "not_opened"
	LeadEngagementClicked    = "clicked"
	LeadEngagementNotClicked = "not_clicked"
	LeadEngagementReplied    = "replied"
	LeadEngagementNotReplied = "not_replied"
	LeadEngagementBounced    = "bounced"
)

Engagement filters accepted by ContactSearchParams.Engagement. Each is a predicate over the contact's progress in one campaign; the Not* forms match only leads that were sent at least one step, so a lead never emailed is neither opened nor not-opened. Opens are human opens only.

View Source
const (
	VerificationEvidenceDelivered        = "delivered"
	VerificationEvidenceOpened           = "opened"
	VerificationEvidenceClicked          = "clicked"
	VerificationEvidenceReplied          = "replied"
	VerificationEvidenceAutoReplied      = "auto_replied"
	VerificationEvidenceBouncedRecipient = "bounced_recipient"
	VerificationEvidenceBouncedOther     = "bounced_other"
)

Kinds of observation in ContactVerificationEvidence.Kind.

View Source
const (
	// ContactSourceUnknown is the honest value for contacts created before
	// attribution existed.
	ContactSourceUnknown = "unknown"
	// ContactSourceManual is a contact added by hand in the dashboard.
	ContactSourceManual = "manual"
	// ContactSourceCampaign is a contact added from a campaign's Leads tab.
	ContactSourceCampaign = "campaign"
	// ContactSourceImport is a file import; the detail is the file name.
	ContactSourceImport = "import"
	// ContactSourceSheetSync is a Google Sheets sync; the detail is the sheet.
	ContactSourceSheetSync = "sheet_sync"
	// ContactSourceAPI is an API-key request; the detail is the key's name.
	ContactSourceAPI = "api"
	// ContactSourceAIAssistant is a contact the AI assistant created.
	ContactSourceAIAssistant = "ai_assistant"
	// ContactSourceForm is a hosted form submission; the detail is the form.
	ContactSourceForm = "form"
	// ContactSourceAutomation is an automation's "create or update contact"
	// action; the detail is the automation's name.
	ContactSourceAutomation = "automation"
)

Where a contact first came from, returned in ContactDetail.Source and on TimelineContactCreated events. It never changes after creation.

View Source
const (
	TimelineEmailSent          = "email_sent"
	TimelineEmailOpened        = "email_opened"
	TimelineEmailClicked       = "email_clicked"
	TimelineEmailReplied       = "email_replied"
	TimelineEmailBounced       = "email_bounced"
	TimelineReplyReceived      = "reply_received"
	TimelineDeliverability     = "deliverability"
	TimelineSuppressed         = "suppressed"
	TimelineNote               = "note"
	TimelineMeetingBooked      = "meeting_booked"
	TimelineMeetingRescheduled = "meeting_rescheduled"
	TimelineMeetingCanceled    = "meeting_canceled"

	// Lifecycle events. TimelineContactCreated carries Source and
	// SourceDetail; the campaign and category events carry the name as it was
	// at the time, so a later rename does not rewrite history.
	TimelineContactCreated  = "contact_created"
	TimelineCampaignAdded   = "campaign_added"
	TimelineCampaignRemoved = "campaign_removed"
	TimelineCategoryAdded   = "category_added"
	TimelineCategoryRemoved = "category_removed"
	// TimelineFormSubmitted carries FormID and FormName.
	TimelineFormSubmitted = "form_submitted"
	// TimelinePageHit is a page view on your own site by a browser tied to the
	// contact through an email-link ticket. Subject is the page title (or its
	// path) and PageHit carries the full view.
	TimelinePageHit = "page_hit"
)

Timeline event types returned in TimelineEvent.Type.

View Source
const (
	// MachineReasonPrefetch is a mail privacy proxy or security gateway.
	MachineReasonPrefetch = "prefetch"
	// MachineReasonInstant is a fetch within ten seconds of the send.
	MachineReasonInstant = "instant"
	// MachineReasonBurst is several links followed within seconds (clicks only).
	MachineReasonBurst = "burst"
)

Why an open or click was classified as automated, returned in TimelineEvent.MachineReason.

View Source
const (
	ActivityEmailSent       = "email_sent"
	ActivityEmailOpened     = "email_opened"
	ActivityEmailClicked    = "email_clicked"
	ActivityEmailReplied    = "email_replied"
	ActivityEmailBounced    = "email_bounced"
	ActivityNoteAdded       = "note_added"
	ActivityNoteUpdated     = "note_updated"
	ActivityDealCreated     = "deal_created"
	ActivityDealStageChange = "deal_stage_changed"
	ActivityDealWon         = "deal_won"
	ActivityDealLost        = "deal_lost"
	ActivityTaskCreated     = "task_created"
	ActivityTaskCompleted   = "task_completed"
	ActivityContactCreated  = "contact_created"
	ActivityContactUpdated  = "contact_updated"
	ActivityFormSubmitted   = "form_submitted"
	ActivityCampaignAdded   = "campaign_added"
	ActivityCampaignRemoved = "campaign_removed"
	ActivityCategoryAdded   = "category_added"
	ActivityCategoryRemoved = "category_removed"
)

Contact activity types returned in ContactActivity.ActivityType.

View Source
const (
	FilterEqual      = "equal"
	FilterStartsWith = "starts_with"
	FilterEndsWith   = "ends_with"
	FilterContains   = "contains"
)

Custom-field match modes for ContactFieldFilter.Type.

View Source
const (
	// FieldOpAdd sets the field only where it is currently absent.
	FieldOpAdd = "ADD"
	// FieldOpEdit overwrites the field's value.
	FieldOpEdit = "EDIT"
	// FieldOpDelete removes the field.
	FieldOpDelete = "DELETE"
	// FieldOpRename renames the key to Value, keeping each contact's value.
	FieldOpRename = "RENAME"
)

Bulk custom-field operations for ContactFieldEdit.Type.

View Source
const (
	// VerificationActionVerify queues a fresh check. Each contact updates as
	// its verdict lands.
	VerificationActionVerify = "verify"
	// VerificationActionMarkDeliverable records a manual "valid" verdict and
	// resumes any campaign of the workspace paused for verification.
	VerificationActionMarkDeliverable = "mark_deliverable"
	// VerificationActionMarkUndeliverable records a manual "invalid" verdict.
	VerificationActionMarkUndeliverable = "mark_undeliverable"
)

Actions accepted by ContactVerificationParams.Action.

View Source
const (
	// NextActionDue carries ScheduledAt, the slot the scheduler would give the
	// step on its next pass. Leads ahead in the queue can still push it later.
	NextActionDue = "due"
	// NextActionWaiting carries NotBefore and a Constraint.
	NextActionWaiting = "waiting"
	// NextActionPaused and NextActionBlocked carry only the Constraint.
	NextActionPaused  = "paused"
	NextActionBlocked = "blocked"
)

How firm a ContactNextAction's timing is, in ContactNextAction.State.

View Source
const (
	ExportFormatCSV  = "csv"
	ExportFormatXLSX = "xlsx"
	ExportFormatJSON = "json"
)

Export formats accepted by ContactExportParams.Format.

View Source
const (
	// ExportScopeAll exports every contact in the organization.
	ExportScopeAll = "all"
	// ExportScopeFiltered exports whatever [ContactExportParams.Filters] match.
	ExportScopeFiltered = "filtered"
	// ExportScopeSelected exports the ids in [ContactExportParams.ContactIDs].
	ExportScopeSelected = "selected"
)

Export scopes accepted by ContactExportParams.Scope.

View Source
const (
	ExportFieldID         = "id"
	ExportFieldEmail      = "email"
	ExportFieldFirstName  = "first_name"
	ExportFieldLastName   = "last_name"
	ExportFieldCompany    = "company"
	ExportFieldPhone      = "phone"
	ExportFieldSubscribed = "subscribed"
	ExportFieldCategories = "categories"
	ExportFieldCampaigns  = "campaigns"
	ExportFieldCreatedAt  = "created_at"
	ExportFieldUpdatedAt  = "updated_at"

	// The lead columns are the contact's engagement inside the one campaign
	// named in [ContactExportParams.Filters].CampaignIDs. They are blank when
	// the filters do not name exactly one campaign.
	ExportFieldLeadStatus  = "lead_status"
	ExportFieldLeadOpened  = "lead_opened"
	ExportFieldLeadClicked = "lead_clicked"
	ExportFieldLeadReplied = "lead_replied"
)

Built-in export column identifiers. A custom field is addressed as "custom:<key>".

View Source
const (
	ImportTargetIgnore     = "ignore"
	ImportTargetEmail      = "email"
	ImportTargetFirstName  = "first_name"
	ImportTargetLastName   = "last_name"
	ImportTargetCompany    = "company"
	ImportTargetPhone      = "phone"
	ImportTargetSubscribed = "subscribed"
	ImportTargetCategories = "categories"
	// ImportTargetVerificationStatus reads a verdict column written by Warmbly
	// or another verification service. A cell nobody recognizes leaves that
	// contact unverified rather than failing the row.
	ImportTargetVerificationStatus = "verification_status"
	// ImportTargetCustom routes the column into a custom field named by
	// [ImportColumnMapping.CustomKey]. The older "custom:<key>" spelling is
	// still accepted.
	ImportTargetCustom = "custom"
)

Where an imported column lands, for ImportColumnMapping.Target.

View Source
const (
	ImportDedupSkip            = "skip"
	ImportDedupUpdate          = "update"
	ImportDedupCreateDuplicate = "create_duplicate"
)

How an import treats a row whose address already exists, for ContactImportParams.Dedup.

View Source
const (
	ResearchQueued       = "queued"
	ResearchRunning      = "running"
	ResearchSucceeded    = "succeeded"
	ResearchFailed       = "failed"
	ResearchNothingFound = "nothing_found"
)

Research run states returned in ResearchRun.Status. ResearchNothingFound is a billable success: the agent looked and honestly found nothing.

View Source
const (
	DealStatusOpen = "open"
	DealStatusWon  = "won"
	DealStatusLost = "lost"
)

Deal states returned in Deal.Status.

View Source
const (
	TaskPriorityLow    = "low"
	TaskPriorityMedium = "medium"
	TaskPriorityHigh   = "high"
	TaskPriorityUrgent = "urgent"
)

CRM task priorities returned in CRMTask.Priority.

View Source
const (
	TaskStatusPending    = "pending"
	TaskStatusInProgress = "in_progress"
	TaskStatusCompleted  = "completed"
	TaskStatusCancelled  = "cancelled" //nolint:misspell // wire value: the API sends "cancelled" here
)

CRM task states returned in CRMTask.Status.

View Source
const (
	ProviderGmail    = "gmail"
	ProviderOutlook  = "outlook"
	ProviderSMTPIMAP = "smtp_imap"
)

Mailbox provider values returned in Email.Provider.

View Source
const (
	MailboxStatusActive   = "active"
	MailboxStatusInactive = "inactive"
	MailboxStatusRevoked  = "revoked"
)

Mailbox connection states returned in Email.Status.

View Source
const (
	// AuthStateUnknown means the domain has not been checked yet, the DNS
	// lookup could not complete, or the domain is special-use and cannot
	// resolve. It never gates sending.
	AuthStateUnknown = "unknown"
	// AuthStatePassing means SPF and DMARC are both published.
	AuthStatePassing = "passing"
	// AuthStateFailing means SPF or DMARC is missing. A domain that stays
	// failing past the instance grace period (72 hours by default, measured
	// from [Email.AuthFailingSince]) stops cold sending and warmup from every
	// mailbox on it.
	AuthStateFailing = "failing"
)

Sending-domain authentication states returned in Email.AuthState.

View Source
const (
	// ErrCodeMailboxAllowanceReached is the 403 returned by every connect path
	// once the workspace's mailbox allowance is full. See
	// [EmailService.Allowance].
	ErrCodeMailboxAllowanceReached = "mailbox_allowance_reached"
	// ErrCodeMailboxWorkerUnreachable is the 503 returned by
	// [EmailService.Delete] when the worker syncing the mailbox could not be
	// told to drop it. Nothing was removed; retry in a moment.
	ErrCodeMailboxWorkerUnreachable = "mailbox_worker_unreachable"
)

Error codes carried in Error.Code by mailbox operations.

View Source
const (
	// SendModeInstant enqueues the message immediately.
	SendModeInstant = "instant"
	// SendModeSmart places the message in the next gap in the mailbox's
	// sending schedule. It follows the mailbox's workday when sending behavior
	// is enabled but is never charged against the cold-send budgets.
	SendModeSmart = "smart"
	// SendModeScheduled sends at [SendEmailParams.ScheduledAt].
	SendModeScheduled = "scheduled"
)

Send modes accepted by SendEmailParams.SendMode.

View Source
const (
	// TrackingStatusVerified: the subdomain resolves to the tracking host and
	// new sends use it.
	TrackingStatusVerified = "verified"
	// TrackingStatusUnset: no custom domain is configured.
	TrackingStatusUnset = "unset"
	// TrackingStatusNoTarget: this install has no tracking host, so there is
	// nothing to point at and nothing can verify.
	TrackingStatusNoTarget = "no_target"
	// TrackingStatusNotFound: the name does not exist yet. The record has not
	// been added or has not propagated (usually minutes, up to an hour).
	TrackingStatusNotFound = "not_found"
	// TrackingStatusWrongTarget: the record exists but points at another
	// host; [TrackingDomainStatus.Observed] says which.
	TrackingStatusWrongTarget = "wrong_target"
	// TrackingStatusLookupError: DNS could not answer. A transient failure
	// never revokes an already verified domain.
	TrackingStatusLookupError = "lookup_error"
	// TrackingStatusPending: stored state that has not been re-resolved, as
	// reported by the read-only [EmailService.GetTrackingDomain].
	TrackingStatusPending = "pending"
)

Machine-readable verdicts returned in TrackingDomainStatus.Status.

View Source
const (
	VerifyStatusValid   = "valid"
	VerifyStatusRisky   = "risky"
	VerifyStatusInvalid = "invalid"
	VerifyStatusUnknown = "unknown"
)

Address verification outcomes returned in VerifyResult.Status.

View Source
const (
	VerifySubStatusCatchAll    = "catch_all"
	VerifySubStatusDisposable  = "disposable"
	VerifySubStatusRole        = "role"
	VerifySubStatusSpamTrap    = "spamtrap"
	VerifySubStatusMailboxFull = "mailbox_full"
	VerifySubStatusNoMX        = "no_mx"
	VerifySubStatusSyntax      = "syntax"
	// VerifySubStatusUndisclosed marks a provider (Microsoft, Yahoo) that
	// answers every RCPT the same way, so a probe cannot judge the mailbox.
	VerifySubStatusUndisclosed = "undisclosed"
)

Reason classes returned in VerifyResult.SubStatus when the verifier can name one.

View Source
const (
	// MailSecurityTLS is implicit TLS: encrypted from the first byte. The
	// convention for SMTP 465 and IMAP 993.
	MailSecurityTLS = "tls"
	// MailSecurityStartTLS is a plaintext greeting upgraded in place with
	// STARTTLS. The convention for SMTP 587, 25 and 2525, and IMAP 143.
	MailSecurityStartTLS = "starttls"
)

Connection security modes accepted in MailboxCredentials.Security. TLS is mandatory either way; the difference is whether it is negotiated before the protocol greeting or upgraded in-band after it.

View Source
const (
	// MailboxBulkConnected: the credentials were validated and the mailbox
	// connected.
	MailboxBulkConnected = "connected"
	// MailboxBulkSkipped: the mailbox was already connected, so re-sending a
	// batch is safe.
	MailboxBulkSkipped = "skipped"
	// MailboxBulkFailed: the row was refused; [MailboxBulkRow.Code] says why.
	MailboxBulkFailed = "failed"
)

Per-row outcomes returned in MailboxBulkRow.Status.

View Source
const (
	// MailboxAllowanceUnlimited: no billing provider, or a plan with no daily
	// send cap. Allowance is nil.
	MailboxAllowanceUnlimited = "unlimited"
	// MailboxAllowanceFree: an unsubscribed workspace's fixed limit.
	MailboxAllowanceFree = "free"
	// MailboxAllowanceOverride: an operator-approved limit-increase request.
	MailboxAllowanceOverride = "override"
	// MailboxAllowancePlan: the plan carries an explicit mailbox limit.
	MailboxAllowancePlan = "plan"
	// MailboxAllowanceFairUse: the plan's daily sends divided by
	// [MailboxAllowance.SendsPerMailbox].
	MailboxAllowanceFairUse = "fair_use"
)

Sources of a workspace's mailbox allowance, returned in MailboxAllowance.Basis.

View Source
const (
	// SendLifecycleActive: in campaign rotation. The default.
	SendLifecycleActive = "active"
	// SendLifecycleResting: pulled out of campaign rotation to recover on
	// warmup traffic alone. Entered automatically when warmup health reaches
	// "throttled" or worse and left on its own once the mailbox has been
	// healthy for three days, or earlier through [EmailService.Release].
	SendLifecycleResting = "resting"
	// SendLifecycleReserve: held back deliberately by the owner through
	// [EmailService.Hold]. Never entered or left automatically.
	SendLifecycleReserve = "reserve"
)

Cold-rotation states returned in SendLifecycleState.State.

View Source
const (
	// SyncBackfillPending: the mailbox is loaded but the import has not run.
	SyncBackfillPending = "pending"
	// SyncBackfillRunning: the import is walking history, newest first, and
	// may be paced.
	SyncBackfillRunning = "running"
	// SyncBackfillComplete: the window is exhausted or the cap was reached.
	SyncBackfillComplete = "complete"
)

Backfill stages returned in MailboxSyncState.BackfillStatus.

View Source
const (
	SyncThrottleBurst        = "burst"
	SyncThrottleHourly       = "hourly"
	SyncThrottleDaily        = "daily"
	SyncThrottleOrgDaily     = "org_daily"
	SyncThrottlePriorityFull = "priority_daily"
)

Exhausted budgets named in MailboxSyncState.ThrottleReason.

View Source
const (
	BehaviorMonday    = 1 << 0
	BehaviorTuesday   = 1 << 1
	BehaviorWednesday = 1 << 2
	BehaviorThursday  = 1 << 3
	BehaviorFriday    = 1 << 4
	BehaviorSaturday  = 1 << 5
	BehaviorSunday    = 1 << 6
	// BehaviorWeekdays is Monday to Friday, the default.
	BehaviorWeekdays = BehaviorMonday | BehaviorTuesday | BehaviorWednesday | BehaviorThursday | BehaviorFriday
	// BehaviorEveryDay is all seven days.
	BehaviorEveryDay = BehaviorWeekdays | BehaviorSaturday | BehaviorSunday
)

Weekday bits for SendingBehavior.Weekdays. The mask is Monday-indexed (bit 0 = Monday), matching the campaign week grid.

View Source
const (
	// ErrCodeBadRequest is the generic 400: malformed JSON, a missing required
	// field, a value out of range.
	ErrCodeBadRequest = "bad_request"
	// ErrCodeUnauthorized is the generic 401: no credential, or one the server
	// would not accept.
	ErrCodeUnauthorized = "unauthorized"
	// ErrCodeForbidden is the generic 403: authenticated, but the credential
	// lacks the permission (or the IP is outside the key's allowlist).
	ErrCodeForbidden = "forbidden"
	// ErrCodeNotFound is the generic 404. It is also what a resource in
	// another workspace returns, so it does not prove non-existence.
	ErrCodeNotFound = "not_found"
	// ErrCodeConflict is the generic 409: the resource already exists, or a
	// unique value collided.
	ErrCodeConflict = "conflict"
	// ErrCodeUnprocessable is the generic 422: the request parsed but failed
	// validation.
	ErrCodeUnprocessable = "unprocessable"
	// ErrCodeRateLimitExceeded is the 429 for the per-key request rate. Honor
	// [Error.RetryAfter]; the client's own retry does.
	ErrCodeRateLimitExceeded = "rate_limit_exceeded"
	// ErrCodeInternalError is the generic 500. Retry once, then quote
	// [Error.RequestID] to support.
	ErrCodeInternalError = "internal_error"
	// ErrCodeNotImplemented is the 501 for a feature this deployment does not
	// build in.
	ErrCodeNotImplemented = "not_implemented"
	// ErrCodeServiceUnavailable is the generic 503, and the only genuinely
	// transient one of the three 503 codes.
	ErrCodeServiceUnavailable = "service_unavailable"

	// ErrCodeInsufficientCredits is the 402 every AI action returns once the
	// workspace's credit balance is spent. Top up or wait for the monthly
	// allowance; retrying changes nothing.
	ErrCodeInsufficientCredits = "insufficient_credits"
	// ErrCodeUsageCapExceeded is a 429 from the AI endpoints: a short-term
	// usage cap, not the request-rate limiter. Unlike
	// [ErrCodeInsufficientCredits] it clears on its own, so retry later.
	ErrCodeUsageCapExceeded = "usage_cap_exceeded"

	// ErrCodeNoOrganization is a 400 raised when a request needs a workspace
	// and the session has none selected. API keys always carry theirs, so this
	// is a dashboard-session condition: every entitlement, limit and
	// suppression rule is workspace-scoped, and a write that would run
	// unscoped is refused rather than run without those checks.
	ErrCodeNoOrganization = "no_organization"
	// ErrCodeStorageLimitReached is the 400 from campaign attachment uploads
	// (and from duplicating a campaign that has them) when the workspace's
	// total attachment storage would pass its quota. The check and the write
	// happen under one lock, so nothing was stored. GET
	// organization/current/limits reports the quota and what is used.
	ErrCodeStorageLimitReached = "storage_limit_reached"
	// ErrCodeMailboxProviderNotConfigured is a 503 that is not transient:
	// the deployment has no OAuth client for the mailbox provider the request
	// named. Self-hosted only. Set the provider's credentials, or connect the
	// mailbox over SMTP and IMAP instead.
	ErrCodeMailboxProviderNotConfigured = "mailbox_provider_not_configured"

	// ErrCodeInvalidLeadStatus and ErrCodeInvalidEngagement are 400s from the
	// contact search and export endpoints for a filter value outside the
	// documented set.
	ErrCodeInvalidLeadStatus = "invalid_lead_status"
	ErrCodeInvalidEngagement = "invalid_engagement"
	// ErrCodeLeadFilterRequiresCampaign is the 400 for setting a lead status
	// or engagement filter without exactly one campaign id. Both describe a
	// contact's standing inside one campaign, so they are meaningless without
	// it.
	ErrCodeLeadFilterRequiresCampaign = "lead_filter_requires_campaign"
	// ErrCodeUnknownVerificationStatus and ErrCodeUnknownVerificationProvider
	// are 400s raised when a stored contact carries a verification status or
	// provider this platform version cannot read — a row written by a newer
	// deployment, or by a service that has since been removed.
	ErrCodeUnknownVerificationStatus   = "unknown_verification_status"
	ErrCodeUnknownVerificationProvider = "unknown_verification_provider"
	// ErrCodeInvalidAction is the 400 from the contact verification endpoint
	// for an action other than verify, mark_deliverable or
	// mark_undeliverable.
	ErrCodeInvalidAction = "invalid_action"
	// ErrCodeNoContacts is the 400 from the contact verification endpoint when
	// the selection resolved to nothing: no explicit contacts, and no campaign
	// with refused leads.
	ErrCodeNoContacts = "no_contacts"

	// ErrCodeRegistrationInviteOnly and ErrCodeRegistrationClosed are 403s
	// describing a deployment's signup policy. They never reveal whether an
	// address already has an account.
	ErrCodeRegistrationInviteOnly = "registration_invite_only"
	ErrCodeRegistrationClosed     = "registration_closed"
	// ErrCodeInvitationInvalid is the 403 for an invitation that is expired,
	// canceled, already used, or was issued for a different address.
	ErrCodeInvitationInvalid = "invitation_invalid"
	// ErrCodeSetupTokenInvalid is the 401 from the first-run claim endpoint
	// for a setup link that is invalid, already used or expired.
	ErrCodeSetupTokenInvalid = "setup_token_invalid"
	// ErrCodeSetupAlreadyComplete is the 403 for claiming an instance that
	// already has an account.
	ErrCodeSetupAlreadyComplete = "setup_already_complete"
	// ErrCodeSSOWrongBrowser is the 401 from the SSO exchange when the handoff
	// code arrives without the binding secret the browser that started the
	// sign-in was given. The handoff is deliberately non-transferable: a
	// forwarded sign-in link cannot sign the recipient in.
	ErrCodeSSOWrongBrowser = "sso_wrong_browser"
)

Stable machine-readable values of the "code" field in the error envelope, for callers that need to branch on the specific condition rather than the HTTP status. Match them with Error.HasCode.

Every response carries one: an endpoint that has nothing more specific to say answers with the generic code for its status class. A few codes live with the service they belong to instead of here — ErrCodeMailboxAllowanceReached, ErrCodeMailboxWorkerUnreachable, ErrCodeListBounceRisk and ErrCodeLeadsUndeliverable.

View Source
const (
	FormStatusDraft     = "draft"
	FormStatusPublished = "published"
	FormStatusArchived  = "archived"
)

Form lifecycle values returned in Form.Status. Only a published form renders and accepts submissions; a draft or archived form's page answers not found, while its data is kept.

View Source
const (
	FormFieldTypeText       = "text"
	FormFieldTypeEmail      = "email"
	FormFieldTypePhone      = "phone"
	FormFieldTypeTextarea   = "textarea"
	FormFieldTypeNumber     = "number"
	FormFieldTypeSelect     = "select"
	FormFieldTypeRadio      = "radio"
	FormFieldTypeCheckboxes = "checkboxes"
	FormFieldTypeCheckbox   = "checkbox"
	FormFieldTypeDate       = "date"
	// FormFieldTypeHidden submits the constant in [FormField.Value] without
	// rendering anything; use it to tag which page or campaign a form sits on.
	FormFieldTypeHidden    = "hidden"
	FormFieldTypeHeading   = "heading"
	FormFieldTypeParagraph = "paragraph"
	FormFieldTypeDivider   = "divider"
	// FormFieldTypePageBreak starts a new page; its label is the page title
	// shown on the form and in the analytics funnel.
	FormFieldTypePageBreak = "page_break"
)

Block types for FormField.Type. Input types collect a value on submit; heading, paragraph, divider and page_break only render.

View Source
const (
	FormMapToFirstName = "first_name"
	FormMapToLastName  = "last_name"
	FormMapToEmail     = "email"
	FormMapToCompany   = "company"
	FormMapToPhone     = "phone"
)

Contact columns a field may fill through FormField.MapTo. At most one field may map to each column, and an email-type field always maps to FormMapToEmail.

View Source
const (
	FormFieldWidthFull = "full"
	FormFieldWidthHalf = "half"
)

Column widths for FormField.Width. Two half-width fields share a row.

View Source
const (
	FormLayoutCard  = "card"
	FormLayoutWide  = "wide"
	FormLayoutSplit = "split"
)

Layouts for FormDesign.Layout: a centered card, fields directly on the page background, or a cover panel beside the form.

View Source
const (
	FormModeClassic = "classic"
	FormModeFocus   = "focus"
)

Modes for FormDesign.Mode: every field of a page at once, or one question per screen.

View Source
const (
	FormFontSystem       = "system"
	FormFontInter        = "inter"
	FormFontSerif        = "serif"
	FormFontMono         = "mono"
	FormFontManrope      = "manrope"
	FormFontSora         = "sora"
	FormFontFraunces     = "fraunces"
	FormFontSpaceGrotesk = "space-grotesk"
)

Font families accepted in FormDesign.FontFamily.

View Source
const (
	FormSizeSmall  = "sm"
	FormSizeMedium = "md"
	FormSizeLarge  = "lg"
)

Sizes shared by FormDesign.ButtonSize and FormDesign.LogoSize.

View Source
const (
	FormSpacingCompact = "compact"
	FormSpacingNormal  = "normal"
	FormSpacingRelaxed = "relaxed"
)

Vertical rhythm values for FormDesign.Spacing.

View Source
const (
	FormAlignLeft    = "left"
	FormAlignCenter  = "center"
	FormAlignBetween = "between"
)

Alignment values. FormDesign.Align takes left or center; FormDesign.HeaderAlign also takes between (logo one side, title the other).

View Source
const (
	FormHeaderPlacementPage   = "page"
	FormHeaderPlacementInline = "inline"
)

Placements for FormDesign.HeaderPlacement: edge to edge above everything, or on the form surface above the fields.

View Source
const (
	FormLogoPositionCard = "card"
	FormLogoPositionPage = "page"
)

Positions for FormDesign.LogoPosition on the card layout: on the card, or above it on the page background.

View Source
const (
	FormBackgroundSizeCover   = "cover"
	FormBackgroundSizeContain = "contain"
	FormBackgroundSizeTile    = "tile"
)

Fits for FormDesign.BackgroundSize, how the uploaded background image covers the page.

View Source
const (
	FormAssetCover      = "cover"
	FormAssetBackground = "background"
)

Asset kinds for FormService.UploadAsset and FormService.DeleteAsset. Every asset must be a PNG or JPG. A logo is capped at 1 MB and 1024px on its longest side; a cover (the split layout's side panel) and a background (behind the whole page) at 4 MB and 2560px.

View Source
const (
	FormStatsRange7Days  = "7d"
	FormStatsRange30Days = "30d"
	FormStatsRange90Days = "90d"
)

Windows accepted by FormService.Stats. Funnel events are kept for 180 days, so 90 days is the widest window the server offers.

View Source
const (
	// FormsDomainStatusVerified means the CNAME resolves to the forms host.
	FormsDomainStatusVerified = "verified"
	// FormsDomainStatusPending is reported by [FormService.Domain] for a
	// stored domain that has not verified yet; it is a read of stored state,
	// not a DNS verdict.
	FormsDomainStatusPending = "pending"
	// FormsDomainStatusUnset means no custom domain is configured.
	FormsDomainStatusUnset = "unset"
	// FormsDomainStatusNoTarget means this install has no forms host, so
	// there is nothing to point a record at (an operator problem).
	FormsDomainStatusNoTarget = "no_target"
	// FormsDomainStatusNotFound means DNS returned no record for the domain.
	FormsDomainStatusNotFound = "not_found"
	// FormsDomainStatusWrongTarget means the record exists but points
	// somewhere other than [FormsDomainStatus.CNAMETarget]; compare
	// [FormsDomainStatus.Observed] to spot the typo.
	FormsDomainStatusWrongTarget = "wrong_target"
	// FormsDomainStatusLookupError means the lookup itself failed
	// (timeout, SERVFAIL); retry rather than treating it as misconfigured.
	FormsDomainStatusLookupError = "lookup_error"
)

Verification outcomes reported in FormsDomainStatus.Status. Only FormsDomainStatusVerified puts form links on the custom domain; every other state leaves them on the shared host, so a half-configured domain never breaks a form.

View Source
const (
	ProviderHubSpot      = "hubspot"
	ProviderSalesforce   = "salesforce"
	ProviderPipedrive    = "pipedrive"
	ProviderClose        = "close"
	ProviderZapier       = "zapier"
	ProviderMake         = "make"
	ProviderN8N          = "n8n"
	ProviderSlack        = "slack"
	ProviderDiscord      = "discord"
	ProviderCalendly     = "calendly"
	ProviderCalCom       = "cal_com"
	ProviderGoogleSheets = "google_sheets"
	// ProviderMillionVerifier is a pay-as-you-go address verifier connected
	// by API key. While it has credits it replaces the built-in check for
	// every contact you import; when they run out the built-in check takes
	// over. The key is validated against the provider before it is stored, so
	// a mistyped key fails [IntegrationService.Connect] rather than silently
	// leaving every contact on the built-in check.
	ProviderMillionVerifier = "millionverifier"
)

Providers available in the integration catalog.

View Source
const (
	ConnectionPending        = "pending"
	ConnectionAuthorizing    = "authorizing"
	ConnectionConnected      = "connected"
	ConnectionDegraded       = "degraded"
	ConnectionReauthRequired = "reauth_required"
	ConnectionDisconnected   = "disconnected"
)

Connection states returned in IntegrationConnection.Status.

View Source
const (
	SyncPush = "push"
	SyncPull = "pull"
	SyncBoth = "both"
)

Data-flow directions for IntegrationConnection.SyncDirection.

View Source
const (
	MeetingBooked      = "booked"
	MeetingRescheduled = "rescheduled"
	MeetingCanceled    = "canceled"
	MeetingCompleted   = "completed"
	MeetingNoShow      = "no_show"
)

Meeting lifecycle states returned in Meeting.Status.

View Source
const (
	MeetingsUpcoming = "upcoming"
	MeetingsPast     = "past"
)

Meeting timeframes accepted by MeetingListParams.Timeframe.

View Source
const (
	// LeadSyncIdle means never synced, or the last sync succeeded.
	LeadSyncIdle = "idle"
	// LeadSyncSyncing means a sync is in flight.
	LeadSyncSyncing = "syncing"
	// LeadSyncError means the last sync failed; see
	// [LeadSyncSource.LastError].
	LeadSyncError = "error"
)

Source states returned in LeadSyncSource.Status.

View Source
const (
	AuthTypeAPIKey = "api_key"
	AuthTypeOAuth  = "oauth"
	AuthTypeJWT    = "jwt"
)

Authentication types returned in Identity.AuthType.

View Source
const (
	DurationMonth = "month"
	DurationYear  = "year"
)

Billing periods returned in Plan.Duration.

View Source
const (
	OAuthAppActive   = "active"
	OAuthAppInactive = "inactive"
)

OAuth application states returned in OAuthApp.Status.

View Source
const (
	// OrgPermManageTeam allows inviting and removing members.
	OrgPermManageTeam uint16 = 1 << iota
	// OrgPermManageBilling allows viewing invoices and changing plans.
	OrgPermManageBilling
	// OrgPermManageCampaigns allows creating and editing campaigns.
	OrgPermManageCampaigns
	// OrgPermManageContacts allows creating and editing contacts.
	OrgPermManageContacts
	// OrgPermManageEmails allows connecting and editing mailboxes.
	OrgPermManageEmails
	// OrgPermViewAnalytics allows viewing reports.
	OrgPermViewAnalytics
	// OrgPermSendCampaigns allows starting campaigns, which sends real mail.
	OrgPermSendCampaigns
	// OrgPermAccessUnibox allows using the unified inbox.
	OrgPermAccessUnibox
	// OrgPermManageSequences allows creating and editing sequence steps.
	OrgPermManageSequences
	// OrgPermManageSettings allows changing organization settings.
	OrgPermManageSettings
	// OrgPermViewCampaigns grants read-only campaign access.
	OrgPermViewCampaigns
	// OrgPermViewContacts grants read-only contact access.
	OrgPermViewContacts
	// OrgPermTransferOwnership allows handing the workspace to another member.
	OrgPermTransferOwnership
	// OrgPermManageAPIKeys allows managing API keys and OAuth applications.
	OrgPermManageAPIKeys
	// OrgPermUseIntegrations allows operating connected integrations — pushing
	// records, running automations — without granting full settings access.
	OrgPermUseIntegrations
	// OrgPermUseAI allows using the AI features, which spend the shared credit
	// balance.
	OrgPermUseAI
)

Organization permission bits. A member's effective grant is the bitwise OR across their assigned roles, which is what travels in Member.Permissions and Role.Permissions.

These gate a human session and are distinct from the API key scopes (the Perm* constants in apikeys.go).

View Source
const (
	RoleOwner   = "owner"
	RoleAdmin   = "admin"
	RoleManager = "manager"
	RoleViewer  = "viewer"
)

Built-in role names returned in Member.Role.

View Source
const (
	// OrgRiskTrusted is the default: nothing is restricted.
	OrgRiskTrusted = "trusted"
	// OrgRiskWatch changes nothing the workspace can feel; evidence is
	// accumulating.
	OrgRiskWatch = "watch"
	// OrgRiskRestricted lowers send caps and confines warmup to the free
	// pool.
	OrgRiskRestricted = "restricted"
	// OrgRiskSuspended stops sending entirely, pending review.
	OrgRiskSuspended = "suspended"
)

Sending postures returned in OrgRisk.State.

View Source
const (
	DeletionStatusPending   = "pending"
	DeletionStatusExecuting = "executing"
	DeletionStatusCompleted = "completed"
	DeletionStatusCancelled = "cancelled" //nolint:misspell // wire value: the API sends "cancelled" here
	DeletionStatusFailed    = "failed"
)

Scheduled-deletion states returned in ScheduledDeletion.Status.

View Source
const (
	OrgTransferQueued    = "queued"
	OrgTransferRunning   = "running"
	OrgTransferCompleted = "completed"
	OrgTransferFailed    = "failed"
	// OrgTransferExpired is export-only: the archive was deleted after its
	// retention window and the row survives as history.
	OrgTransferExpired = "expired"
)

Workspace archive job states returned in OrgExportJob.Status and OrgImportJob.Status.

View Source
const (
	// OrgDataGroupCore is the workspace itself and is always included.
	OrgDataGroupCore        = "core"
	OrgDataGroupContacts    = "contacts"
	OrgDataGroupCampaigns   = "campaigns"
	OrgDataGroupCRM         = "crm"
	OrgDataGroupAutomations = "automations"
	OrgDataGroupAI          = "ai"
	OrgDataGroupWarmup      = "warmup"
	OrgDataGroupInbox       = "inbox"
	OrgDataGroupSending     = "sending"
	OrgDataGroupEvents      = "events"
	OrgDataGroupLogs        = "logs"
	// OrgDataGroupBilling is exported but never applied verbatim on import:
	// the destination instance owns billing.
	OrgDataGroupBilling = "billing"
)

Data groups an archive can carry, in OrgExportParams.Groups, OrgImportParams.Groups and OrgDataGroupInfo.Key. Read the live catalog with OrganizationService.TransferGroups; these are the keys it uses.

View Source
const (
	// OrgImportSkip keeps the row that is already there. The safe default: an
	// import never destroys data that was not in the archive.
	OrgImportSkip = "skip"
	// OrgImportOverwrite replaces the existing row with the archive's.
	OrgImportOverwrite = "overwrite"
)

What to do with a row the destination already has, in OrgImportParams.ConflictStrategy.

View Source
const (
	LimitRequestPending   = "pending"
	LimitRequestApproved  = "approved"
	LimitRequestRejected  = "rejected"
	LimitRequestCancelled = "cancelled" //nolint:misspell // wire value: the API sends "cancelled" here
)

Limit-request states returned in LimitRequest.Status.

View Source
const (
	LimitFieldMaxCampaigns       = "max_campaigns"
	LimitFieldMaxActiveCampaigns = "max_active_campaigns"
	LimitFieldMaxTeamMembers     = "max_team_members"
	LimitFieldMaxEmailAccounts   = "max_email_accounts"
	LimitFieldMaxContacts        = "max_contacts"
	LimitFieldDailyCampaignLimit = "daily_campaign_limit"
)

Ceilings a limit increase can be requested for, in LimitRequestParams.Field and LimitRequest.Field.

View Source
const (
	// UnsubscribeModeInherit is valid only on a campaign: it follows the
	// organization's [UnsubscribeSettings]. Sent as the organization mode it
	// is normalized to [UnsubscribeModeText].
	UnsubscribeModeInherit = "inherit"
	// UnsubscribeModeText appends a plain sentence inviting a reply to opt
	// out. It is the default: it reads as a personal email, and a reply that
	// asks to stop is detected and honored automatically.
	UnsubscribeModeText = "text"
	// UnsubscribeModeLink appends a sentence with a real, signed unsubscribe
	// link, unique to the recipient and campaign and valid for a year.
	UnsubscribeModeLink = "link"
	// UnsubscribeModeOff appends nothing.
	UnsubscribeModeOff = "off"
)

In-body opt-out modes for UnsubscribeSettings.Mode and Campaign.UnsubscribeMode. The List-Unsubscribe header is a separate per-campaign flag (Campaign.UnsubscribeHeader).

View Source
const (
	DeliverabilityEventBounce      = "bounce"
	DeliverabilityEventComplaint   = "complaint"
	DeliverabilityEventUnsubscribe = "unsubscribe"
	DeliverabilityEventOpen        = "open"
	DeliverabilityEventClick       = "click"
	DeliverabilityEventReply       = "reply"
)

Deliverability event types accepted by DeliverabilityEventParams.EventType.

View Source
const (
	// PoolLinkCodePending is a code nobody has settled yet.
	PoolLinkCodePending = "pending"
	// PoolLinkCodeApproved is a code a member approved whose instance has not
	// yet collected its token. Poll delivers the token exactly once and moves
	// the code to [PoolLinkCodeClaimed].
	PoolLinkCodeApproved = "approved"
	// PoolLinkCodeClaimed is a spent code: the instance fetched its token.
	// Polling it again fails with code "pool_link_code_used".
	PoolLinkCodeClaimed = "claimed"
	// PoolLinkCodeDenied is a code a member declined. Polling it fails with
	// code "pool_link_denied".
	PoolLinkCodeDenied = "denied"
)

Lifecycle of one device-code handshake, as reported in PoolLinkCode.Status and PoolLinkPollResult.Status.

View Source
const (
	// PoolLinkTierFree warms a capped number of linked mailboxes at no cost.
	PoolLinkTierFree = "free"
	// PoolLinkTierPaid lifts the mailbox cap; the workspace's regular hosted
	// plan also counts as paid.
	PoolLinkTierPaid = "paid"
)

Tiers reported in PoolLinkPlan.Tier.

View Source
const (
	// SegmentMaxConditions is the most conditions one segment may hold.
	SegmentMaxConditions = 50
	// SegmentMaxListValues is the most entries a list condition may hold.
	SegmentMaxListValues = 200
	// SegmentMaxMemberBatch is the most contact IDs one SetMembers or
	// MemberModes call accepts.
	SegmentMaxMemberBatch = 1000
	// SegmentMaxOverrides caps the Overrides listing.
	SegmentMaxOverrides = 500
)

Segment limits enforced by the server.

View Source
const (
	// TemplateIssueWarn is worth fixing but will not sink the message.
	TemplateIssueWarn = "warn"
	// TemplateIssueHigh materially risks landing in spam.
	TemplateIssueHigh = "high"
)

Severities returned in TemplateIssue.Severity.

View Source
const (
	FolderInbox   = "inbox"
	FolderSent    = "sent"
	FolderDrafts  = "drafts"
	FolderArchive = "archive"
	FolderSpam    = "spam"
	FolderTrash   = "trash"
)

Canonical mail folders. Every message is filed in exactly one, derived from where the provider placed it at sync time. They are accepted by UniboxListParams.Folder and UniboxService.MarkFolderSeen, and reported in UniboxMessage.Folder and UniboxOverview.Folders.

View Source
const (
	// DirectionSent matches messages sent from the workspace's own mailboxes.
	DirectionSent = "sent"
	// DirectionReceived matches everything else.
	DirectionReceived = "received"
)

Message directions accepted by UniboxListParams.Direction.

View Source
const (
	AgentDraftPending   = "pending"
	AgentDraftApproved  = "approved"
	AgentDraftDiscarded = "discarded"
)

Inbox-agent draft states returned in AgentDraft.Status.

View Source
const (
	// MatchAny matches every address on that side.
	MatchAny = "any"
	// MatchDomain matches an exact domain.
	MatchDomain = "domain"
	// MatchTLD matches a top-level domain.
	MatchTLD = "tld"
	// MatchProvider matches a mail provider classification, for example
	// "gmail" or "outlook".
	MatchProvider = "provider"
)

Match modes for the sender and recipient sides of a routing rule.

View Source
const (
	// WebhookSignatureHeader carries the signature as "t=<unix>,v1=<hex>",
	// where the hex digest is an HMAC-SHA256 over "<unix>." followed by the raw
	// request body, keyed by the endpoint's secret.
	WebhookSignatureHeader = "X-Warmbly-Signature"
	// WebhookEventHeader carries the delivered event type.
	WebhookEventHeader = "X-Warmbly-Event"
	// WebhookEventIDHeader carries the event's id, which is stable across
	// retries and so is the right key for receiver-side deduplication.
	WebhookEventIDHeader = "X-Warmbly-Event-Id"
	// WebhookChallengeHeader is where a receiver echoes the ownership
	// challenge back. Read the token from the verified [EventEndpointTest]
	// payload rather than from this header on the request: the header is a
	// convenience copy an attacker could forge, while the body is signed.
	WebhookChallengeHeader = "X-Warmbly-Webhook-Challenge"
)

Headers set on every webhook delivery.

View Source
const (
	WebhookCatCampaign       = "Campaign"
	WebhookCatWarmup         = "Warmup"
	WebhookCatDeliverability = "Deliverability"
	WebhookCatInbox          = "Inbox"
	WebhookCatEmailAccount   = "Mailbox"
	WebhookCatContact        = "Contact"
	WebhookCatCRM            = "CRM"
	WebhookCatAutomation     = "Automation"
	WebhookCatMeeting        = "Meeting"
	WebhookCatTeam           = "Team & access"
	WebhookCatBulk           = "Bulk operations"
	WebhookCatWorkspace      = "Workspace"
	WebhookCatDeveloper      = "Developer"
)

Event categories used to group WebhookEventDescriptor entries.

View Source
const (
	// DeliveryPending is queued for its next attempt.
	DeliveryPending = "pending"
	// DeliveryInFlight is being attempted right now.
	DeliveryInFlight = "in_flight"
	// DeliveryDelivered succeeded.
	DeliveryDelivered = "delivered"
	// DeliveryFailed will be retried.
	DeliveryFailed = "failed"
	// DeliveryAbandoned exhausted its attempts.
	DeliveryAbandoned = "abandoned"
)

Delivery states returned in WebhookDelivery.Status.

View Source
const (
	// WebsiteConsentExplicit records nothing until the page calls
	// warmbly('consent', 'granted'). The default; enforced server-side as
	// well, so a stale snippet cannot downgrade it.
	WebsiteConsentExplicit = "explicit"
	// WebsiteConsentImplicit records on load. The workspace asserts its own
	// lawful basis by choosing it.
	WebsiteConsentImplicit = "implicit"
)

Consent modes returned in WebsiteTrackingSettings.ConsentMode.

View Source
const (
	WebsiteLocationNone    = "none"
	WebsiteLocationCountry = "country"
	WebsiteLocationCity    = "city"
)

How much IP-derived location is kept, in WebsiteTrackingSettings.LocationPrecision.

View Source
const (
	WebsiteRetentionMinDays     = 7
	WebsiteRetentionMaxDays     = 365
	WebsiteRetentionDefaultDays = 90
)

Bounds for WebsiteTrackingSettings.RetentionDays.

View Source
const DefaultWebhookTolerance = 5 * time.Minute

DefaultWebhookTolerance is how far a delivery's signature timestamp may drift from local time before WebhookService.ConstructEvent rejects it.

View Source
const MaxAutomationDepth = 5

MaxAutomationDepth is how many automations may run in a chain before the server stops following it.

View Source
const MaxSMTPIMAPBulkRows = 50

MaxSMTPIMAPBulkRows is the most rows one EmailService.ConnectSMTPIMAPBulk call accepts.

View Source
const OrgPermAll uint16 = 0xFFFF

OrgPermAll is every organization permission, which is what the owner holds.

View Source
const SegmentCustomFieldPrefix = "custom."

SegmentCustomFieldPrefix addresses a contact custom field in a condition: "custom.industry" filters on the custom field "industry". Custom fields are text fields. The workspace's current keys are returned by SegmentService.Fields.

View Source
const SuppressionMaxEntries = 5000

SuppressionMaxEntries is the most entries one Add call accepts.

View Source
const Version = "0.3.1"

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

Variables

View Source
var (
	// ErrBadRequest is returned for HTTP 400 responses.
	ErrBadRequest = &Error{StatusCode: http.StatusBadRequest}
	// ErrUnauthorized is returned for HTTP 401 responses (missing or invalid
	// credentials).
	ErrUnauthorized = &Error{StatusCode: http.StatusUnauthorized}
	// ErrForbidden is returned for HTTP 403 responses (authenticated but not
	// permitted).
	ErrForbidden = &Error{StatusCode: http.StatusForbidden}
	// ErrNotFound is returned for HTTP 404 responses.
	ErrNotFound = &Error{StatusCode: http.StatusNotFound}
	// ErrConflict is returned for HTTP 409 responses.
	ErrConflict = &Error{StatusCode: http.StatusConflict}
	// ErrUnprocessable is returned for HTTP 422 responses (validation failed).
	ErrUnprocessable = &Error{StatusCode: http.StatusUnprocessableEntity}
	// ErrRateLimited is returned for HTTP 429 responses.
	ErrRateLimited = &Error{StatusCode: http.StatusTooManyRequests}
	// ErrServer matches any HTTP 5xx response.
	ErrServer = &Error{StatusCode: http.StatusInternalServerError}
)

Sentinel errors for matching API failures with errors.Is. They carry only a status code; the concrete error returned from a call carries the full body.

View Source
var (
	// ErrInvalidWebhookSignature is returned when the delivery signature does
	// not match the computed HMAC.
	ErrInvalidWebhookSignature = errors.New("warmbly: invalid webhook signature")
	// ErrWebhookSignatureExpired is returned when the signature is valid but
	// its timestamp falls outside the tolerance, which defeats replay.
	ErrWebhookSignatureExpired = errors.New("warmbly: webhook signature timestamp outside tolerance")
)

Errors returned when a delivery fails verification.

View Source
var DefaultEndpoint = OAuth2Endpoint{
	AuthURL:   "https://app.warmbly.com/oauth/authorize",
	TokenURL:  "https://api.warmbly.com/v1/oauth/token",
	RevokeURL: "https://api.warmbly.com/v1/oauth/revoke",
}

DefaultEndpoint is the production Warmbly authorization server.

View Source
var ErrCLIAuthDenied = errors.New("warmbly: CLI sign-in was denied")

ErrCLIAuthDenied is returned by AuthService.WaitForCLIAuth when a member declined the request.

View Source
var ErrNoMorePages = errors.New("warmbly: no more pages")

ErrNoMorePages is returned by Page.Next when the current page is the last.

Functions

func Bool

func Bool(v bool) *bool

Bool, Int, Float, String and Time return pointers to their arguments, for filling the optional fields of parameter structs inline:

params := &warmbly.EmailUpdateParams{Name: warmbly.String("Sales")}

func ComputeWebhookSignature

func ComputeWebhookSignature(payload []byte, secret string, timestamp time.Time) string

ComputeWebhookSignature returns the expected WebhookSignatureHeader value for a payload, signing timestamp and secret: "t=<unix>,v1=<hex>", where the digest is HMAC-SHA256 over "<unix>." followed by the raw payload.

func Float64

func Float64(v float64) *float64

Float64 returns a pointer to v.

func GenerateVerifier

func GenerateVerifier() string

GenerateVerifier returns a high-entropy PKCE code verifier (RFC 7636): the base64url encoding of 32 random bytes (43 characters). Generate one per authorization request, pass it to S256ChallengeOption, and supply the same value to [Exchange] via VerifierOption.

func Int

func Int(v int) *int

Int returns a pointer to v.

func Int64

func Int64(v int64) *int64

Int64 returns a pointer to v.

func S256ChallengeFromVerifier

func S256ChallengeFromVerifier(verifier string) string

S256ChallengeFromVerifier derives the S256 code challenge from a verifier.

func String

func String(v string) *string

String returns a pointer to v.

func Time

func Time(v time.Time) *time.Time

Time returns a pointer to v.

func VerifyWebhookSignature

func VerifyWebhookSignature(payload []byte, signatureHeader, secret string) bool

VerifyWebhookSignature reports whether signatureHeader is a valid signature for payload under secret. The digest comparison is constant-time.

It does not check the timestamp for freshness; use ConstructWebhookEvent (or VerifyWebhookSignatureAt) to also defeat replay.

func VerifyWebhookSignatureAt

func VerifyWebhookSignatureAt(payload []byte, signatureHeader, secret string) (time.Time, bool)

VerifyWebhookSignatureAt verifies a signature and returns the timestamp it was signed at, so a caller can apply its own freshness policy.

Types

type ABAnalysis

type ABAnalysis struct {
	CampaignID string             `json:"campaign_id"`
	Variants   []ABVariantMetrics `json:"variants"`
	// WinnerID and WinnerName are nil until a winner clears the sample-size
	// and confidence bar.
	WinnerID   *string `json:"winner_id"`
	WinnerName *string `json:"winner_name"`
	// WinningRule is the metric the comparison was decided on.
	WinningRule string `json:"winning_rule"`
	// Confidence is a qualitative reading, for example "high" or "low".
	Confidence string `json:"confidence"`
}

ABAnalysis compares the arms of a campaign's A/B test.

type ABTestingSettings

type ABTestingSettings struct {
	Enabled bool `json:"enabled"`
	// DefaultWinningRule is the metric a winner is picked on, for example
	// "reply_rate".
	DefaultWinningRule string `json:"default_winning_rule"`
	AutoPromoteWinner  bool   `json:"auto_promote_winner"`
	MinSampleSize      int    `json:"min_sample_size"`
}

ABTestingSettings governs how A/B variants are compared and promoted.

type ABVariant

type ABVariant struct {
	ID         string `json:"id"`
	CampaignID string `json:"campaign_id"`
	// StepID scopes the variant to a single step; nil applies it campaign-wide.
	StepID *string `json:"step_id"`
	Name   string  `json:"name"`
	// Weight is the variant's share of the split.
	Weight    int    `json:"weight"`
	Subject   string `json:"subject"`
	BodyHTML  string `json:"body_html"`
	BodyPlain string `json:"body_plain"`
	// IsControl marks the baseline arm the others are measured against.
	IsControl bool           `json:"is_control"`
	IsActive  bool           `json:"is_active"`
	Metadata  map[string]any `json:"metadata,omitempty"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
}

ABVariant is one arm of a campaign's A/B test.

type ABVariantCreateParams

type ABVariantCreateParams struct {
	Name      string         `json:"name"`
	StepID    string         `json:"step_id,omitempty"`
	Weight    *int           `json:"weight,omitempty"`
	Subject   string         `json:"subject,omitempty"`
	BodyHTML  string         `json:"body_html,omitempty"`
	BodyPlain string         `json:"body_plain,omitempty"`
	IsControl *bool          `json:"is_control,omitempty"`
	IsActive  *bool          `json:"is_active,omitempty"`
	Metadata  map[string]any `json:"metadata,omitempty"`
}

ABVariantCreateParams creates an A/B variant. Only Name is required.

type ABVariantMetrics

type ABVariantMetrics struct {
	VariantID   string  `json:"variant_id"`
	VariantName string  `json:"variant_name"`
	TotalSent   int64   `json:"total_sent"`
	Opened      int64   `json:"opened"`
	Clicked     int64   `json:"clicked"`
	Replied     int64   `json:"replied"`
	Bounced     int64   `json:"bounced"`
	OpenRate    float64 `json:"open_rate"`
	ClickRate   float64 `json:"click_rate"`
	ReplyRate   float64 `json:"reply_rate"`
	BounceRate  float64 `json:"bounce_rate"`
}

ABVariantMetrics is one variant's engagement in an ABAnalysis.

type ABVariantUpdateParams

type ABVariantUpdateParams struct {
	Name      *string        `json:"name,omitempty"`
	Weight    *int           `json:"weight,omitempty"`
	Subject   *string        `json:"subject,omitempty"`
	BodyHTML  *string        `json:"body_html,omitempty"`
	BodyPlain *string        `json:"body_plain,omitempty"`
	IsControl *bool          `json:"is_control,omitempty"`
	IsActive  *bool          `json:"is_active,omitempty"`
	Metadata  map[string]any `json:"metadata,omitempty"`
}

ABVariantUpdateParams updates an A/B variant. Nil fields are unchanged.

type AIDraft

type AIDraft struct {
	// Text is the drafted message. It is empty when the model asked a
	// clarifying Question instead.
	Text string `json:"text,omitempty"`
	// Question is a single clarifying question, returned when the request gave
	// the model too little to work with.
	Question string `json:"question,omitempty"`
	// CreditsCharged is what this draft cost; CreditsRemaining is the balance
	// afterwards.
	CreditsCharged   int    `json:"credits_charged"`
	CreditsRemaining int    `json:"credits_remaining"`
	TokensUsed       int    `json:"tokens_used"`
	Model            string `json:"model"`
	// Grounding reports what context the draft was written against. It is only
	// returned by [UniboxService.DraftCompose].
	Grounding *AIDraftGrounding `json:"grounding,omitempty"`
}

AIDraft is a generated draft. It is never sent by the call that produced it.

type AIDraftGrounding

type AIDraftGrounding struct {
	// Contact is true when a contact record was found for the recipient.
	Contact bool `json:"contact"`
	// History is how many prior messages with the address were included. The
	// model reads their stored text, not just the preview lines.
	History int `json:"history"`
	// VoiceProfile is true when the workspace voice profile was applied.
	VoiceProfile bool `json:"voice_profile"`
}

AIDraftGrounding reports which context sources fed a compose draft.

type AISkill

type AISkill struct {
	ID          string `json:"id"`
	OrgID       string `json:"org_id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	// Content is the instruction text handed to the model.
	Content string `json:"content"`
	// Enabled reports whether the skill is currently applied.
	Enabled   bool      `json:"enabled"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

AISkill is one workspace playbook.

type AIVariableParams

type AIVariableParams struct {
	// Mode is [AIVariableInstant] or [AIVariableResearch].
	Mode   string `json:"mode,omitempty"`
	Prompt string `json:"prompt"`
	Tone   string `json:"tone,omitempty"`
	// WebSearch lets a research-mode preview search the web.
	WebSearch bool `json:"web_search,omitempty"`
	// ContactID is the contact to render against.
	ContactID string `json:"contact_id,omitempty"`
	// ContextBefore and ContextAfter are the surrounding copy, each clamped
	// server-side so a caller cannot inflate the prompt.
	ContextBefore string `json:"context_before,omitempty"`
	ContextAfter  string `json:"context_after,omitempty"`
}

AIVariableParams previews a per-recipient AI variable block against one contact, so the editor can show what a merge tag will actually produce.

type APIKey

type APIKey struct {
	ID             string `json:"id"`
	UserID         string `json:"user_id"`
	OrganizationID string `json:"organization_id"`
	Name           string `json:"name"`
	Description    string `json:"description,omitempty"`
	// KeyPrefix and KeySuffix are the non-secret ends of the key, shown so a
	// human can tell two keys apart.
	KeyPrefix string `json:"key_prefix"`
	KeySuffix string `json:"key_suffix"`
	// Permissions is the granted scope bitmask; test it with [APIKey.Can].
	Permissions uint64 `json:"permissions"`
	// AllowedIPs restricts use to these IPs or CIDR ranges. Empty means any.
	AllowedIPs []string `json:"allowed_ips,omitempty"`
	// AllowedEmailAccounts restricts the key to specific mailbox ids. Empty
	// means every mailbox in the organization.
	AllowedEmailAccounts []string `json:"allowed_email_accounts,omitempty"`
	// RateLimitPerMinute is the per-key request ceiling, enforced as a
	// sliding window. Zero means the server default (60).
	RateLimitPerMinute int `json:"rate_limit_per_minute"`
	// Status is [APIKeyStatusActive], [APIKeyStatusRevoked] or
	// [APIKeyStatusExpired].
	Status        string     `json:"status"`
	LastUsedAt    *time.Time `json:"last_used_at"`
	LastRequestIP *string    `json:"last_request_ip"`
	ExpiresAt     *time.Time `json:"expires_at"`
	RevokedAt     *time.Time `json:"revoked_at"`
	RevokedReason *string    `json:"revoked_reason"`
	CreatedAt     time.Time  `json:"created_at"`
	UpdatedAt     time.Time  `json:"updated_at"`
}

APIKey is an API key as returned by the API. The full secret is never included; see APIKeyWithSecret, returned only by APIKeyService.Create.

func (*APIKey) Can

func (k *APIKey) Can(perms uint64) bool

Can reports whether the key holds every bit in perms.

if key.Can(warmbly.PermSendCampaigns) { ... }

func (*APIKey) CanAny

func (k *APIKey) CanAny(perms uint64) bool

CanAny reports whether the key holds at least one bit in perms.

func (*APIKey) Revoked

func (k *APIKey) Revoked() bool

Revoked reports whether the key has been revoked.

type APIKeyAnalytics

type APIKeyAnalytics struct {
	// APIKeyID is the key the report covers, or the zero UUID for the
	// organization-wide report.
	APIKeyID string    `json:"api_key_id"`
	From     time.Time `json:"from"`
	To       time.Time `json:"to"`
	// Interval is one of the Interval* constants.
	Interval  string               `json:"interval"`
	Buckets   []APIKeyUsageBucket  `json:"buckets"`
	Endpoints []APIKeyEndpointStat `json:"endpoints"`
	Total     int64                `json:"total"`
	Errors    int64                `json:"errors"`
}

APIKeyAnalytics is call volume, latency and error rate over a window, both bucketed over time and broken down by endpoint.

type APIKeyAnalyticsParams

type APIKeyAnalyticsParams struct {
	// From defaults to 24 hours before To; To defaults to now. The window may
	// not exceed 90 days.
	From time.Time
	To   time.Time
	// Interval is [IntervalMinute], [IntervalHour] or [IntervalDay]. Leave it
	// empty to let the server pick from the window's span: minutes up to two
	// hours, hours up to a week, days beyond that. The interval it settled on
	// comes back in [APIKeyAnalytics.Interval].
	Interval string
}

APIKeyAnalyticsParams selects the window and granularity of a usage report.

type APIKeyCreateParams

type APIKeyCreateParams struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	// Permissions is the scope bitmask, for example
	// [PermReadOnly] or PermReadCampaigns|PermSendCampaigns. It must name at
	// least one scope, and only scopes the server knows: a mask carrying an
	// unrecognized bit is refused rather than silently narrowed, so a stale
	// client cannot grant a scope it does not understand.
	Permissions uint64 `json:"permissions"`
	// AllowedIPs restricts the key to these IPs or CIDR ranges (at most
	// [MaxAllowedIPs]); AllowedEmailAccounts to these mailbox ids (at most
	// [MaxAllowedEmailAccounts]). Empty means no restriction.
	AllowedIPs           []string `json:"allowed_ips,omitempty"`
	AllowedEmailAccounts []string `json:"allowed_email_accounts,omitempty"`
	// RateLimitPerMinute caps requests per minute for this key, between
	// [MinRateLimitPerMinute] and [MaxRateLimitPerMinute]; zero keeps the
	// server default of [DefaultRateLimitPerMinute].
	RateLimitPerMinute int        `json:"rate_limit_per_minute,omitempty"`
	ExpiresAt          *time.Time `json:"expires_at,omitempty"`
}

APIKeyCreateParams provisions an API key. Name (at most 255 characters) and Permissions are required.

type APIKeyEndpointStat

type APIKeyEndpointStat struct {
	Endpoint     string  `json:"endpoint"`
	Method       string  `json:"method"`
	Count        int64   `json:"count"`
	ErrorCount   int64   `json:"error_count"`
	AvgLatencyMS float64 `json:"avg_latency_ms"`
}

APIKeyEndpointStat is one endpoint's share of the traffic.

type APIKeyService

type APIKeyService service

APIKeyService manages the organization's API keys.

API keys authenticate server-to-server requests and are prefixed "wmbly_". The secret is shown exactly once, at creation; afterwards only a prefix and suffix are retrievable.

func (*APIKeyService) Analytics

Analytics returns call volume and latency for a single key.

func (*APIKeyService) Create

Create provisions a new API key. The returned APIKeyWithSecret is the only time the plaintext credential is available.

func (*APIKeyService) Get

func (s *APIKeyService) Get(ctx context.Context, id string, opts ...RequestOption) (*APIKey, *Response, error)

Get retrieves a single API key by ID.

func (*APIKeyService) List

func (s *APIKeyService) List(ctx context.Context, params *ListOptions, opts ...RequestOption) (*Page[APIKey], error)

List returns a page of API keys.

func (*APIKeyService) Logs

func (s *APIKeyService) Logs(ctx context.Context, id string, params *ListOptions, opts ...RequestOption) (*Page[APIKeyUsageLog], error)

Logs returns a page of individual requests made with a key, most recent first. The server caps ListOptions.Limit at 200 and defaults to 50.

func (*APIKeyService) Permissions

func (s *APIKeyService) Permissions(ctx context.Context, opts ...RequestOption) (*PermissionCatalog, *Response, error)

Permissions lists every scope bit the API exposes, together with the preset masks.

func (*APIKeyService) Revoke

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

Revoke permanently revokes an API key. The reason is stored on the key and may be empty.

func (*APIKeyService) RevokeSelf added in v0.3.0

func (s *APIKeyService) RevokeSelf(ctx context.Context, reason string, opts ...RequestOption) (*Response, error)

RevokeSelf revokes the key this client is authenticated with. It is the one key route that needs no scope: a credential must always be able to end itself, which is what a CLI logout promises. The reason is stored on the key and may be empty (the server records that the key revoked itself). A session caller gets a 400 — there is no key in that request to end; use AuthService.Logout. Every request after this one fails with 401.

func (*APIKeyService) Update

func (s *APIKeyService) Update(ctx context.Context, id string, params *APIKeyUpdateParams, opts ...RequestOption) (*APIKey, *Response, error)

Update modifies an existing API key.

func (*APIKeyService) UsageAnalytics

func (s *APIKeyService) UsageAnalytics(ctx context.Context, params *APIKeyAnalyticsParams, opts ...RequestOption) (*APIKeyAnalytics, *Response, error)

UsageAnalytics returns call volume and latency across every key in the organization.

func (*APIKeyService) UsageSummary

func (s *APIKeyService) UsageSummary(ctx context.Context, opts ...RequestOption) (*APIKeyUsageSummary, *Response, error)

UsageSummary returns a rollup of the organization's key usage.

type APIKeyUpdateParams

type APIKeyUpdateParams struct {
	Name                 *string   `json:"name,omitempty"`
	Description          *string   `json:"description,omitempty"`
	Permissions          *uint64   `json:"permissions,omitempty"`
	AllowedIPs           *[]string `json:"allowed_ips,omitempty"`
	AllowedEmailAccounts *[]string `json:"allowed_email_accounts,omitempty"`
	RateLimitPerMinute   *int      `json:"rate_limit_per_minute,omitempty"`
}

APIKeyUpdateParams updates an API key. Nil fields are left unchanged; an empty (non-nil) AllowedIPs or AllowedEmailAccounts clears the restriction. Permissions is validated the same way as on create: non-zero, and only scopes the server knows.

type APIKeyUsageBucket

type APIKeyUsageBucket struct {
	Bucket       time.Time `json:"bucket"`
	Total        int64     `json:"total"`
	Success      int64     `json:"success"`
	ClientErrors int64     `json:"client_errors"`
	ServerErrors int64     `json:"server_errors"`
	AvgLatencyMS float64   `json:"avg_latency_ms"`
}

APIKeyUsageBucket is one time bucket of API traffic.

type APIKeyUsageLog

type APIKeyUsageLog struct {
	ID             string    `json:"id"`
	APIKeyID       string    `json:"api_key_id"`
	Endpoint       string    `json:"endpoint"`
	Method         string    `json:"method"`
	IPAddress      string    `json:"ip_address"`
	UserAgent      string    `json:"user_agent"`
	ResponseCode   int       `json:"response_code"`
	ResponseTimeMS int       `json:"response_time_ms"`
	CreatedAt      time.Time `json:"created_at"`
}

APIKeyUsageLog is a single recorded API request made with a key.

type APIKeyUsageSummary

type APIKeyUsageSummary struct {
	ActiveKeys  int `json:"active_keys"`
	RevokedKeys int `json:"revoked_keys"`
	ExpiredKeys int `json:"expired_keys"`
	// Requests24h and Errors24h cover the last rolling day.
	Requests24h     int64      `json:"requests_24h"`
	Errors24h       int64      `json:"errors_24h"`
	AvgLatencyMS24h float64    `json:"avg_latency_ms_24h"`
	LastCallAt      *time.Time `json:"last_call_at"`
}

APIKeyUsageSummary is a rollup of the organization's key usage.

type APIKeyWithSecret

type APIKeyWithSecret struct {
	APIKey
	// Secret is the full plaintext credential (prefixed "wmbly_"). Capture it
	// now; it cannot be retrieved again.
	Secret string `json:"secret"`
}

APIKeyWithSecret is an API key together with its plaintext secret, returned only by APIKeyService.Create.

type APIUsage

type APIUsage struct {
	TotalCalls int64 `json:"total_calls"`
	DailyLimit int64 `json:"daily_limit"`
	// TopEndpoints are the busiest endpoints, each carrying its own endpoint
	// and count keys.
	TopEndpoints []map[string]any `json:"top_endpoints,omitempty"`
}

APIUsage is API call volume against the plan's daily limit.

type AccountDailyUsage

type AccountDailyUsage struct {
	Date          string `json:"date"`
	CampaignSent  int64  `json:"campaign_sent"`
	CampaignLimit int64  `json:"campaign_limit"`
	WarmupSent    int64  `json:"warmup_sent"`
	WarmupLimit   int64  `json:"warmup_limit"`
}

AccountDailyUsage is a mailbox's sending against today's caps. The warmup pair is omitted (zero) for a mailbox that is not warming.

type AccountError

type AccountError struct {
	ID        string `json:"id"`
	ErrorCode string `json:"error_code"`
	Severity  string `json:"severity"`
	Title     string `json:"title"`
	Message   string `json:"message"`
	// ActionRequired tells the mailbox owner what to do about it, when there
	// is something to do.
	ActionRequired *string   `json:"action_required,omitempty"`
	CreatedAt      time.Time `json:"created_at"`
}

AccountError is a recent error recorded against a mailbox.

type AccountHealth

type AccountHealth struct {
	Status string `json:"status"`
	// Score runs 0 to 100, higher being healthier.
	Score  int      `json:"score"`
	Issues []string `json:"issues,omitempty"`
}

AccountHealth is a mailbox's health verdict.

type AccountHealthTotals

type AccountHealthTotals struct {
	TotalAccounts   int `json:"total_accounts"`
	HealthyAccounts int `json:"healthy_accounts"`
	WarningAccounts int `json:"warning_accounts"`
	ErrorAccounts   int `json:"error_accounts"`
}

AccountHealthTotals counts mailboxes by health band.

type AccountStatus

type AccountStatus struct {
	ID           string            `json:"id"`
	Email        string            `json:"email"`
	Provider     string            `json:"provider"`
	Status       string            `json:"status"`
	LastSyncedAt *time.Time        `json:"last_synced_at"`
	Health       AccountHealth     `json:"health"`
	Errors       []AccountError    `json:"errors,omitempty"`
	DailyUsage   AccountDailyUsage `json:"daily_usage"`
	// WarmupStatus is the warmup ramp, present once warmup has ever been
	// enabled (running or paused).
	WarmupStatus *WarmupStatus `json:"warmup_status,omitempty"`
	// WarmupHealth is the mailbox's standing in the warmup pool. It is folded
	// into Health.Score and nil when the mailbox is not in a pool.
	WarmupHealth *WarmupHealth `json:"warmup_health,omitempty"`
	// InCampaign reports whether the mailbox is attached to a running campaign.
	// When true a low-volume health-check warmup keeps running even if warmup
	// is paused or off.
	InCampaign bool `json:"in_campaign"`
	// SendLifecycle is present only when the mailbox is NOT in cold rotation
	// (resting or held in reserve); an active mailbox needs no explanation.
	SendLifecycle *SendLifecycleState `json:"send_lifecycle,omitempty"`
	// ColdRamp is present only while the warmup-to-cold graduation ceiling
	// holds today's cold allowance below the mailbox's own campaign limit.
	ColdRamp *ColdRamp `json:"cold_ramp,omitempty"`
}

AccountStatus is one mailbox's operational state: health, recent errors, how much of its daily allowance it has used, and anything currently holding its volume down.

type ActivityEvent

type ActivityEvent struct {
	// Type is the engagement, for example "open", "click" or "reply".
	Type         string    `json:"type"`
	CampaignID   string    `json:"campaign_id"`
	CampaignName string    `json:"campaign_name"`
	ContactEmail string    `json:"contact_email"`
	ContactID    string    `json:"contact_id"`
	Timestamp    time.Time `json:"timestamp"`
	// Link is the URL that was clicked, on click events.
	Link string `json:"link,omitempty"`
}

ActivityEvent is one recent engagement event on the dashboard feed.

type AddToCampaignConfig added in v0.3.0

type AddToCampaignConfig struct {
	CampaignID string `json:"campaign_id"`
}

AddToCampaignConfig is the config of an ActionAddToCampaign node. CampaignID is required and validated on save.

func (*AddToCampaignConfig) Config added in v0.3.0

func (c *AddToCampaignConfig) Config() (json.RawMessage, error)

Config renders the struct as an AutomationNode.Config value.

type AdvisorAction

type AdvisorAction struct {
	// Tool is the registry name of what would run, and Args its payload.
	Tool string          `json:"tool"`
	Args json.RawMessage `json:"args"`
	// Label is the button text.
	Label string `json:"label"`
	// Auto marks a fix autopilot may apply unattended. It is true only for a
	// bounded, reversible settings change in the safe direction.
	Auto bool `json:"auto,omitempty"`
	// Preview is the exact before and after.
	Preview []AdvisorPreviewChange `json:"preview,omitempty"`
	// Undo, when set, reverts the action.
	Undo *AdvisorUndo `json:"undo,omitempty"`
}

AdvisorAction is a finding's one-click remedy. It runs as the calling member with their permissions enforced.

type AdvisorAgentResult

type AdvisorAgentResult struct {
	FindingID string `json:"finding_id"`
	// Applied is true only when the agent actually changed something. A run
	// that looked and decided nothing was wrong reports false and the finding
	// stays open.
	Applied bool `json:"applied"`
	// Summary is the agent's own account of what it did.
	Summary string `json:"summary"`
	// Steps are the calls it made, in order, so they can be checked against
	// the audit log.
	Steps []string `json:"steps,omitempty"`
}

AdvisorAgentResult is what an agent fix reports back.

type AdvisorFinding

type AdvisorFinding struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`

	// DetectorKey identifies the check that produced the finding.
	DetectorKey string `json:"detector_key"`
	// Category is one of the AdvisorCategory* constants, Severity one of the
	// Advisor{Critical,High,Medium,Low} constants, and Surface one of the
	// AdvisorSurface* constants.
	Category string `json:"category"`
	Severity string `json:"severity"`
	Surface  string `json:"surface"`

	// EntityType, EntityID and EntityLabel name what the finding is about.
	EntityType  string  `json:"entity_type,omitempty"`
	EntityID    *string `json:"entity_id,omitempty"`
	EntityLabel string  `json:"entity_label,omitempty"`

	// ParentType and ParentID name where the finding belongs when that differs
	// from its subject: a step's copy problem belongs to its campaign.
	ParentType string  `json:"parent_type,omitempty"`
	ParentID   *string `json:"parent_id,omitempty"`

	// Status is one of the AdvisorStatus* constants.
	Status string `json:"status"`
	// Impact ranks the finding against the others.
	Impact int `json:"impact"`

	Title string `json:"title"`
	// GroupTitle names the finding when several of its kind are listed
	// together, with a {count} placeholder. Empty means it always stands alone.
	GroupTitle string `json:"group_title,omitempty"`
	Detail     string `json:"detail"`
	Remedy     string `json:"remedy"`
	// Steps is the manual how-to, set only on findings with no one-click fix.
	Steps []string `json:"steps,omitempty"`
	// AgentFixable reports whether [AdvisorService.AgentFix] could resolve
	// this. It is false for anything outside the platform, such as a DNS
	// record, so a client shows the steps rather than a button that cannot
	// succeed.
	AgentFixable bool `json:"agent_fixable"`
	// Snippets are exact values to paste somewhere the platform cannot reach.
	Snippets []AdvisorSnippet `json:"snippets,omitempty"`
	// Narrated is false while the finding still shows built-in fallback copy
	// rather than AI narration. It is fully usable either way.
	Narrated bool `json:"narrated"`

	// Evidence is the detector's raw supporting data.
	Evidence json.RawMessage `json:"evidence,omitempty"`
	// Action is the one-click remedy, when there is one.
	Action *AdvisorAction `json:"action,omitempty"`

	FirstSeenAt time.Time  `json:"first_seen_at"`
	LastSeenAt  time.Time  `json:"last_seen_at"`
	ResolvedAt  *time.Time `json:"resolved_at,omitempty"`

	SnoozedUntil  *time.Time `json:"snoozed_until,omitempty"`
	DismissedAt   *time.Time `json:"dismissed_at,omitempty"`
	DismissReason string     `json:"dismiss_reason,omitempty"`

	AppliedAt     *time.Time `json:"applied_at,omitempty"`
	AppliedBy     *string    `json:"applied_by,omitempty"`
	AppliedResult string     `json:"applied_result,omitempty"`
}

AdvisorFinding is one piece of advice about one entity.

type AdvisorListParams

type AdvisorListParams struct {
	// Surface is one of the AdvisorSurface* constants.
	Surface string
	// Category is one of the AdvisorCategory* constants.
	Category string
	// EntityType and EntityID narrow to one entity.
	EntityType string
	EntityID   string
	// Statuses matches any of the AdvisorStatus* constants. The default is
	// open findings.
	Statuses []string
	// Limit caps the rows returned, from 1 to 200.
	Limit int
}

AdvisorListParams filters the findings list.

type AdvisorPreviewChange

type AdvisorPreviewChange struct {
	Field string `json:"field"`
	From  string `json:"from"`
	To    string `json:"to"`
}

AdvisorPreviewChange is one field an action would change.

type AdvisorService

type AdvisorService service

AdvisorService reads and acts on the Advisor: continuous checks on the workspace's sending posture, surfaced as findings on the thing they are about.

A finding either carries a one-click AdvisorAction, can be handed to an agent with AdvisorService.AgentFix, or comes with AdvisorFinding.Steps and AdvisorFinding.Snippets for what the platform cannot reach itself, such as a DNS record.

Applying a fix runs through the same permission checks as doing it by hand, so a viewer sees the advice and gets a clean 403 if they try to apply it.

func (*AdvisorService) AgentFix

func (s *AdvisorService) AgentFix(ctx context.Context, id string, opts ...RequestOption) (*AdvisorAgentResult, *Response, error)

AgentFix hands a finding to a bounded agent for the cases a settings change cannot resolve. It spends AI credits, acts as the calling member inside their permissions, and reports the calls it actually made.

This route is session-only: it is not reachable with an API key.

func (*AdvisorService) Apply

Apply performs a finding's one-click remedy and returns the updated finding. It runs as the calling member, so it fails with a 403 if they could not make the same change by hand.

func (*AdvisorService) Dismiss

func (s *AdvisorService) Dismiss(ctx context.Context, id, reason string, opts ...RequestOption) (*Response, error)

Dismiss rejects a finding. The dismissal sticks until the underlying condition clears and later recurs.

func (*AdvisorService) Feedback

func (s *AdvisorService) Feedback(ctx context.Context, id string, helpful bool, reason string, opts ...RequestOption) (*Response, error)

Feedback records whether a finding was helpful, which feeds detector tuning.

func (*AdvisorService) Recommendations

func (s *AdvisorService) Recommendations(ctx context.Context, params *AdvisorListParams, opts ...RequestOption) ([]AdvisorFinding, *Response, error)

Recommendations returns the workspace's findings.

func (*AdvisorService) Refresh

func (s *AdvisorService) Refresh(ctx context.Context, opts ...RequestOption) (*AdvisorSummary, *Response, error)

Refresh triggers a re-evaluation and returns the summary. The re-run happens in the background, so the summary may still reflect the previous pass.

func (*AdvisorService) Settings

func (s *AdvisorService) Settings(ctx context.Context, opts ...RequestOption) (*AdvisorSettings, *Response, error)

Settings returns which Advisor checks are enabled for the workspace.

func (*AdvisorService) Snooze

func (s *AdvisorService) Snooze(ctx context.Context, id string, days int, opts ...RequestOption) (*Response, error)

Snooze hides a finding for a bounded number of days, from 1 to 90. An unbounded snooze would be a dismissal in disguise, so use AdvisorService.Dismiss for that.

func (*AdvisorService) Summary

func (s *AdvisorService) Summary(ctx context.Context, opts ...RequestOption) (*AdvisorSummary, *Response, error)

Summary returns the workspace health score and the per-surface badge counts.

func (*AdvisorService) Undo

Undo reverts an applied remedy.

func (*AdvisorService) UpdateSettings

func (s *AdvisorService) UpdateSettings(ctx context.Context, settings *AdvisorSettings, opts ...RequestOption) (*AdvisorSettings, *Response, error)

UpdateSettings replaces the Advisor configuration. Silencing checks for a whole workspace is governance, so this route is session-only and needs the manage-settings permission.

type AdvisorSettings

type AdvisorSettings struct {
	OrganizationID string `json:"organization_id"`
	Enabled        bool   `json:"enabled"`
	// MutedCategories holds AdvisorCategory* values, MutedDetectors holds
	// detector keys.
	MutedCategories []string `json:"muted_categories"`
	MutedDetectors  []string `json:"muted_detectors"`
	// MinSeverity hides anything below it.
	MinSeverity string `json:"min_severity"`
	// Autopilot applies auto-safe fixes on its own. It is off by default.
	Autopilot bool `json:"autopilot"`
	// AutopilotActorID is the member autopilot acts as. It is set to whoever
	// switched it on, and autopilot stops if they leave.
	AutopilotActorID *string   `json:"autopilot_actor_id,omitempty"`
	UpdatedAt        time.Time `json:"updated_at"`
}

AdvisorSettings controls which checks run for the workspace. Reading it is an analytics read; changing it is workspace governance and is session-only.

type AdvisorSnippet

type AdvisorSnippet struct {
	// Label names the field in the target system's own words.
	Label string `json:"label"`
	Value string `json:"value"`
	// Note is the caveat that trips people up on this specific field.
	Note string `json:"note,omitempty"`
}

AdvisorSnippet is an exact value to paste into something outside Warmbly, such as a DNS record.

type AdvisorSummary

type AdvisorSummary struct {
	// Score is 0-100, falling as open findings accumulate and weighted so one
	// critical finding outweighs a pile of low-severity nits.
	Score    int                   `json:"score"`
	Total    int                   `json:"total"`
	Critical int                   `json:"critical"`
	High     int                   `json:"high"`
	Medium   int                   `json:"medium"`
	Low      int                   `json:"low"`
	Surfaces []AdvisorSurfaceCount `json:"surfaces"`
	// LastRunAt is nil before the first evaluation.
	LastRunAt *time.Time `json:"last_run_at,omitempty"`
}

AdvisorSummary is the workspace rollup behind the health score and the per-tab badges.

type AdvisorSurfaceCount

type AdvisorSurfaceCount struct {
	// Surface is one of the AdvisorSurface* constants.
	Surface string `json:"surface"`
	// Total is every open finding on the surface; Critical and High set the
	// badge's tone.
	Total    int `json:"total"`
	Critical int `json:"critical"`
	High     int `json:"high"`
}

AdvisorSurfaceCount is one surface's badge payload.

type AdvisorUndo

type AdvisorUndo struct {
	Tool string          `json:"tool"`
	Args json.RawMessage `json:"args"`
}

AdvisorUndo reverts an applied action.

type AgentBlock

type AgentBlock struct {
	// Kind is "text" or "tool".
	Kind string `json:"kind"`
	Text string `json:"text,omitempty"`

	Tool        string `json:"tool,omitempty"`
	ArgsSummary string `json:"args_summary,omitempty"`
	Result      string `json:"result,omitempty"`

	// EntityType, EntityID and OpenURL point at something the tool created, so
	// a client can link straight to it.
	EntityType string `json:"entity_type,omitempty"`
	EntityID   string `json:"entity_id,omitempty"`
	OpenURL    string `json:"open_url,omitempty"`

	// Done is false while a tool step is still running.
	Done bool `json:"done"`
}

AgentBlock is one piece of a turn.

type AgentDraft

type AgentDraft struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	EmailAccountID string `json:"email_account_id"`
	// OwnerUserID is the mailbox owner; an approved send goes out as them.
	OwnerUserID string `json:"owner_user_id"`
	ThreadID    string `json:"thread_id"`
	// SourceMessageID is the inbound message being replied to.
	SourceMessageID *string `json:"source_message_id,omitempty"`
	ContactID       *string `json:"contact_id,omitempty"`
	CampaignID      *string `json:"campaign_id,omitempty"`

	ToAddr  string `json:"to_addr"`
	Subject string `json:"subject"`
	// InReplyTo is the Message-ID referenced on send.
	InReplyTo string `json:"in_reply_to"`
	Body      string `json:"body"`
	// IntentClass is how the agent read the inbound reply, and Confidence how
	// sure it was.
	IntentClass string  `json:"intent_class"`
	Confidence  float64 `json:"confidence"`
	Model       string  `json:"model"`
	// Status is one of the AgentDraft* constants.
	Status    string    `json:"status"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

AgentDraft is a reply the inbox agent suggested for an inbound human reply. It is never sent until someone approves it.

type AgentEvent

type AgentEvent struct {
	// Type is one of the AgentEvent* constants.
	Type string `json:"type"`
	// Text is reply content, whole or incremental.
	Text string `json:"text,omitempty"`

	// Tool, Risk, ArgsSummary and ToolCallID describe a tool step.
	Tool        string `json:"tool,omitempty"`
	Risk        string `json:"risk,omitempty"`
	ArgsSummary string `json:"args_summary,omitempty"`
	ToolCallID  string `json:"tool_call_id,omitempty"`
	Result      string `json:"result,omitempty"`

	// Iteration counts the assistant's turns within the run, and Budget caps
	// them.
	Iteration int `json:"iteration,omitempty"`
	Budget    int `json:"budget,omitempty"`
	// CreditsRemaining is the balance after this step; FreeModel means nothing
	// was charged.
	CreditsRemaining int  `json:"credits_remaining,omitempty"`
	FreeModel        bool `json:"free_model,omitempty"`

	// Code and Message are set on [AgentEventError].
	Code    string `json:"code,omitempty"`
	Message string `json:"message,omitempty"`

	// EntityType, EntityID and OpenURL point at something a tool created.
	EntityType string `json:"entity_type,omitempty"`
	EntityID   string `json:"entity_id,omitempty"`
	OpenURL    string `json:"open_url,omitempty"`
}

AgentEvent is one step of a streamed run. Which fields are set depends on Type.

type AgentSession

type AgentSession struct {
	ID     string `json:"id"`
	OrgID  string `json:"org_id"`
	UserID string `json:"user_id"`
	Title  string `json:"title"`
	// UserName attributes the conversation in a workspace-shared history; see
	// [Organization.AssistantSharedHistory].
	UserName  string              `json:"user_name,omitempty"`
	Context   AgentSessionContext `json:"context"`
	CreatedAt time.Time           `json:"created_at"`
	UpdatedAt time.Time           `json:"updated_at"`
}

AgentSession is one assistant conversation.

type AgentSessionContext

type AgentSessionContext struct {
	// Page and Resource are what the caller was looking at, folded into the
	// prompt so "this campaign" resolves.
	Page     string `json:"page,omitempty"`
	Resource string `json:"resource,omitempty"`
	// Model is the provider model resolved for this session.
	Model string `json:"model,omitempty"`
	// FreeModel reports that the run used a free or local backend, in which
	// case no credits were charged.
	FreeModel bool `json:"free_model,omitempty"`
	// Pending is the tool call awaiting approval when a run is paused.
	Pending *PendingTool `json:"pending,omitempty"`
}

AgentSessionContext is what the assistant knows about the session beyond its messages.

type AgentTool added in v0.3.0

type AgentTool struct {
	// Name is the identifier to pass to [AgentToolService.Call].
	Name string `json:"name"`
	// Description is the model-facing explanation of what the tool does and
	// when to use it.
	Description string `json:"description"`
	// InputSchema is the JSON Schema of the argument object, kept verbatim so
	// it drops into a function-calling client unchanged. It is always an
	// object schema; a tool with no arguments has an empty properties map.
	InputSchema json.RawMessage `json:"input_schema"`
}

AgentTool is one registry tool the caller is permitted to use, as returned by AgentToolService.List.

The server does not report a tool's risk class or the scope it checks; the list is already filtered to what the credential allows, so everything in it is callable with the same credential.

func (AgentTool) Function added in v0.3.0

func (t AgentTool) Function() AgentToolFunction

Function converts the tool to the OpenAI function-calling shape, without a round trip to the server. It is what AgentToolService.ListFunctions returns, built locally.

type AgentToolCallResult added in v0.3.0

type AgentToolCallResult struct {
	// Name echoes the tool that ran.
	Name string `json:"name"`
	// Result is the tool's output. It is the tool's JSON embedded directly
	// when the tool produced JSON (every registry tool does today) and a JSON
	// string otherwise, so it is always valid JSON and can be fed back to the
	// model as the tool-call result unchanged.
	Result json.RawMessage `json:"result"`
}

AgentToolCallResult is what one tool call returned.

func (*AgentToolCallResult) Decode added in v0.3.0

func (r *AgentToolCallResult) Decode(v any) error

Decode unmarshals the tool's output into v, for callers that know the shape a particular tool produces.

type AgentToolFunction added in v0.3.0

type AgentToolFunction struct {
	// Type is always "function".
	Type     string                `json:"type"`
	Function AgentToolFunctionSpec `json:"function"`
}

AgentToolFunction is a tool in OpenAI function-calling form: {"type":"function","function":{...}}. The objects go verbatim into an OpenAI-compatible tools array or a Hermes <tools> block.

func AgentToolFunctions added in v0.3.0

func AgentToolFunctions(tools []AgentTool) []AgentToolFunction

AgentToolFunctions converts a tool list to OpenAI function-calling objects, for a client that fetched the default shape and also wants the manifest.

type AgentToolFunctionSpec added in v0.3.0

type AgentToolFunctionSpec struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	// Parameters is the argument JSON Schema, the same bytes as
	// [AgentTool.InputSchema].
	Parameters json.RawMessage `json:"parameters"`
}

AgentToolFunctionSpec is the function half of an AgentToolFunction.

type AgentToolService added in v0.3.0

type AgentToolService service

AgentToolService is the AI tool registry over plain HTTP, for function-calling agents that do not speak MCP: Hermes-style models, OpenAI-compatible frameworks, LangChain executors, plain scripts. It is the same registry that POST /v1/mcp exposes, gated the same way, so a tool behaves identically whichever door it came through.

There is no route-level scope on these endpoints. Each tool enforces its own permission — the API-key scope for key and OAuth callers, the organization permission for session callers — and AgentToolService.List only ever shows what the caller may use, so the whole list can be handed to a model as-is. Send-class tools (anything that puts real mail on the wire) are never listed and never callable here: an agent wired through this surface can read, search, label and draft, but a human presses send.

A call runs as the caller with their permissions, exactly as the matching REST route would; a tool can never do more than the credential could through the normal API. Write-class tools audit like the routes they wrap.

func (*AgentToolService) Call added in v0.3.0

func (s *AgentToolService) Call(ctx context.Context, name string, args any, opts ...RequestOption) (*AgentToolCallResult, *Response, error)

Call runs one tool by name. args is the tool's argument object exactly as the model produced it — a map, a struct, or the raw json.RawMessage from the model's tool call — and must encode to a JSON object matching the tool's AgentTool.InputSchema. A nil args sends no body, which the server treats as no arguments.

Failures map onto the package sentinels:

  • ErrNotFound (404, code "not_found"): no such tool. A send-class tool answers the same way on purpose, so a name a model saw elsewhere (for example the dashboard assistant's send_reply) is indistinguishable from a typo here.
  • ErrForbidden (403, code "forbidden"): the credential lacks the tool's permission. It would not have appeared in AgentToolService.List.
  • ErrBadRequest (400, code "bad_request"): the body was not valid JSON.
  • ErrUnprocessable (422, code "unprocessable"): the tool itself failed — arguments that decoded but did not validate, a missing record, an entitlement the workspace lacks. The Error.Message is written for the model to read and react to; feed it back as the tool result rather than treating it as a transport error.

Calls run under the write rate limit and honor WithIdempotencyKey like every other mutating request, so a retried call with the same key replays the stored result instead of running the tool again.

func (*AgentToolService) List added in v0.3.0

func (s *AgentToolService) List(ctx context.Context, opts ...RequestOption) ([]AgentTool, *Response, error)

List returns the tools the caller's credential may use, in registry order, in the default {name, description, input_schema} shape. Use AgentTool.Function or AgentToolFunctions to convert locally, or AgentToolService.ListFunctions to have the server do it.

Fetch it once at session start: the set only changes when the credential's scopes do. The read rate limit applies.

func (*AgentToolService) ListFunctions added in v0.3.0

func (s *AgentToolService) ListFunctions(ctx context.Context, opts ...RequestOption) ([]AgentToolFunction, *Response, error)

ListFunctions is AgentToolService.List with the server rendering OpenAI function-calling objects (GET /ai/tools?format=openai), ready for an OpenAI-compatible tools array or a Hermes <tools> block. The filtering is identical; only the shape differs.

type AgentTranscript

type AgentTranscript struct {
	Title string      `json:"title"`
	Turns []AgentTurn `json:"turns"`
	// Pending is set when the conversation is waiting on an approval.
	Pending *PendingTool `json:"pending,omitempty"`
	// FreeModel reports that the session ran without charging credits.
	FreeModel bool `json:"free_model"`
}

AgentTranscript is a session rehydrated for display.

type AgentTurn

type AgentTurn struct {
	// Role is "user" or "assistant".
	Role   string       `json:"role"`
	Blocks []AgentBlock `json:"blocks"`
}

AgentTurn is one user or assistant turn. An assistant turn interleaves text and tool steps in the order they happened.

type AnalyticsService

type AnalyticsService service

AnalyticsService reads aggregate analytics: the organization dashboard, per-campaign engagement, warmup progress, deliverability health, mailbox status and plan usage. Every operation is read-only.

The date-range endpoints take plain calendar days, which the SDK formats as YYYY-MM-DD in UTC.

func (*AnalyticsService) Account

func (s *AnalyticsService) Account(ctx context.Context, id string, opts ...RequestOption) (*AccountStatus, *Response, error)

Account returns one mailbox's operational status, including its warmup ramp, any cold-ramp ceiling and any lifecycle hold.

func (*AnalyticsService) Accounts

func (s *AnalyticsService) Accounts(ctx context.Context, opts ...RequestOption) ([]AccountStatus, *Response, error)

Accounts returns the operational status of every mailbox.

func (*AnalyticsService) Campaign

Campaign returns one campaign's engagement, broken down by step.

func (*AnalyticsService) CampaignDaily

func (s *AnalyticsService) CampaignDaily(ctx context.Context, id string, from, to time.Time, opts ...RequestOption) ([]DailyStat, *Response, error)

CampaignDaily returns a campaign's day-by-day engagement over a date range.

func (*AnalyticsService) CampaignHourly

func (s *AnalyticsService) CampaignHourly(ctx context.Context, id string, date time.Time, opts ...RequestOption) ([]HourlyStat, *Response, error)

CampaignHourly returns a campaign's hour-by-hour engagement for one day. A zero date reports today.

func (*AnalyticsService) CompareCampaigns

func (s *AnalyticsService) CompareCampaigns(ctx context.Context, ids []string, from, to time.Time, opts ...RequestOption) (*CampaignComparison, *Response, error)

CompareCampaigns compares up to ten campaigns over one date range.

func (*AnalyticsService) Dashboard

func (s *AnalyticsService) Dashboard(ctx context.Context, period string, opts ...RequestOption) (*DashboardAnalytics, *Response, error)

Dashboard returns the organization-wide engagement summary. Period is Period7Days, Period30Days or Period90Days; anything else falls back to Period7Days.

func (*AnalyticsService) Deliverability

func (s *AnalyticsService) Deliverability(ctx context.Context, from, to time.Time, opts ...RequestOption) (*DeliverabilityDashboard, *Response, error)

Deliverability returns the organization's sending health over a window. Zero times default to the last seven days.

func (*AnalyticsService) Usage

func (s *AnalyticsService) Usage(ctx context.Context, period string, opts ...RequestOption) (*UsageOverview, *Response, error)

Usage returns the organization's consumption against its plan. Period is UsagePeriodDay, UsagePeriodWeek or UsagePeriodMonth.

func (*AnalyticsService) Warmup

func (s *AnalyticsService) Warmup(ctx context.Context, emailID string, from, to time.Time, opts ...RequestOption) (*WarmupAnalytics, *Response, error)

Warmup returns warmup progress over a date range. Pass an empty emailID for the whole organization.

type AssistantService

type AssistantService service

AssistantService drives the workspace AI assistant: conversations, their transcripts, and the streamed runs that answer a message.

The assistant acts as the calling member. Every tool it runs is re-checked against that member's permissions, and anything that sends mail or spends money pauses for an explicit approval. Runs spend AI credits.

These routes are session-only — no API key can drive the assistant — and need the use-AI permission.

func (*AssistantService) Approve

func (s *AssistantService) Approve(ctx context.Context, sessionID, decision string, fn func(*AgentEvent) bool, opts ...RequestOption) (*Response, error)

Approve answers a paused approval and streams the rest of the run. The decision is ApprovalApprove, ApprovalDeny or ApprovalAlwaysAllow.

func (*AssistantService) ClearSessions

func (s *AssistantService) ClearSessions(ctx context.Context, opts ...RequestOption) (*Response, error)

ClearSessions removes every conversation the caller can see.

func (*AssistantService) ConnectMCPServer

func (s *AssistantService) ConnectMCPServer(ctx context.Context, params *MCPServerParams, opts ...RequestOption) (*MCPServer, *Response, error)

ConnectMCPServer registers an external MCP server and discovers its tools.

func (*AssistantService) CreateSession

func (s *AssistantService) CreateSession(ctx context.Context, page, resource string, opts ...RequestOption) (*AgentSession, *Response, error)

CreateSession opens a new conversation, optionally anchored to what the user is looking at.

func (*AssistantService) DeleteMCPServer

func (s *AssistantService) DeleteMCPServer(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteMCPServer disconnects a server and forgets its credential.

func (*AssistantService) DeleteSession

func (s *AssistantService) DeleteSession(ctx context.Context, sessionID string, opts ...RequestOption) (*Response, error)

DeleteSession removes one conversation.

func (*AssistantService) MCPServers

func (s *AssistantService) MCPServers(ctx context.Context, opts ...RequestOption) ([]MCPServer, *Response, error)

MCPServers returns the workspace's connected MCP servers.

func (*AssistantService) RefreshMCPServer

func (s *AssistantService) RefreshMCPServer(ctx context.Context, id string, opts ...RequestOption) (*MCPServer, *Response, error)

RefreshMCPServer re-discovers a connected server's tools.

func (*AssistantService) SendMessage

func (s *AssistantService) SendMessage(ctx context.Context, sessionID string, params *MessageParams, fn func(*AgentEvent) bool, opts ...RequestOption) (*Response, error)

SendMessage runs the assistant on a message and streams the run, calling fn for each step as it arrives. Return false from fn to stop reading early.

The run pauses at AgentEventApprovalRequired and ends there; answer it with AssistantService.Approve, which streams the remainder.

_, err := client.Assistant.SendMessage(ctx, sessionID,
	&warmbly.MessageParams{Text: "Which campaigns are bouncing?"},
	func(ev *warmbly.AgentEvent) bool {
		if ev.Type == warmbly.AgentEventTextDelta {
			fmt.Print(ev.Text)
		}
		return true
	})

func (*AssistantService) Sessions

func (s *AssistantService) Sessions(ctx context.Context, params *ListOptions, opts ...RequestOption) (*Page[AgentSession], error)

Sessions returns a page of the caller's conversations, newest first. With Organization.AssistantSharedHistory on, it returns the whole workspace's.

func (*AssistantService) Transcript

func (s *AssistantService) Transcript(ctx context.Context, sessionID string, opts ...RequestOption) (*AgentTranscript, *Response, error)

Transcript returns a conversation rehydrated for display, including any pending approval.

func (*AssistantService) UpdateMCPServer

func (s *AssistantService) UpdateMCPServer(ctx context.Context, id string, params *MCPServerUpdateParams, opts ...RequestOption) (*MCPServer, *Response, error)

UpdateMCPServer changes a connected server's name, credential or enabled state.

type AuditActor

type AuditActor struct {
	ID        string `json:"id"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Email     string `json:"email"`
}

AuditActor is the member who performed an audited action.

type AuditLog

type AuditLog struct {
	ID    string `json:"id"`
	OrgID string `json:"org_id"`
	// UserID is the acting member's id; Actor carries their hydrated profile
	// when the record could be joined.
	UserID string      `json:"user_id"`
	Actor  *AuditActor `json:"actor,omitempty"`

	ActionDate time.Time `json:"action_date"`
	// Action is what happened, for example [AuditActionUpdate].
	Action string `json:"action"`
	// EntityType is what it happened to, for example [AuditEntityCampaign].
	EntityType string  `json:"entity_type"`
	EntityID   *string `json:"entity_id,omitempty"`

	IPAddress string `json:"ip_address"`
	UserAgent string `json:"user_agent"`
	// Changes records the fields that moved; Metadata carries extra context.
	Changes   map[string]string `json:"changes,omitempty"`
	Metadata  map[string]string `json:"metadata,omitempty"`
	Timestamp time.Time         `json:"timestamp"`
}

AuditLog is one entry in the organization's audit trail.

type AuditLogListParams

type AuditLogListParams struct {
	ListOptions
	// ActorID limits the trail to one acting member.
	ActorID string
	// EntityID limits it to one entity, and EntityType to one kind of entity.
	EntityID   string
	EntityType string
	// Action limits it to one action.
	Action string
	// Date selects a single UTC day. StartDate and EndDate override it with an
	// explicit range.
	Date      time.Time
	StartDate time.Time
	EndDate   time.Time
}

AuditLogListParams filters and paginates the audit trail.

type AuditLogService

type AuditLogService service

AuditLogService reads the organization's audit trail: who changed what, when and from where. The trail is org-scoped server-side, so one organization can never read another's.

func (*AuditLogService) List

func (s *AuditLogService) List(ctx context.Context, params *AuditLogListParams, opts ...RequestOption) (*Page[AuditLog], error)

List returns a page of audit-trail entries, newest first. The page size must be between 10 and 200; zero uses the server default of 50.

type AuthCodeOption

type AuthCodeOption interface {
	// contains filtered or unexported methods
}

AuthCodeOption customizes the parameters of an authorization or token request.

func S256ChallengeOption

func S256ChallengeOption(verifier string) AuthCodeOption

S256ChallengeOption adds an S256 PKCE challenge derived from verifier to an OAuth2Config.AuthCodeURL call.

func SetAuthURLParam

func SetAuthURLParam(key, value string) AuthCodeOption

SetAuthURLParam sets an arbitrary key/value parameter on the request.

func VerifierOption

func VerifierOption(verifier string) AuthCodeOption

VerifierOption supplies the PKCE code verifier to an OAuth2Config.Exchange call.

type AuthConfig added in v0.3.0

type AuthConfig struct {
	// Captcha reports whether a Turnstile token is verified. When false do not
	// collect one: an air-gapped install cannot reach the challenge service.
	Captcha bool `json:"captcha"`
	// PasswordLogin is false when the deployment authenticates only through
	// SSO or passkeys; [AuthService.Login] and [AuthService.Register] are then
	// refused.
	PasswordLogin bool `json:"password_login"`
	// LoginCode is [LoginCodeAlways], [LoginCodeNewDevice] or [LoginCodeOff].
	LoginCode string `json:"login_code"`
	// Registration is [RegistrationOpen], [RegistrationInviteOnly] or
	// [RegistrationClosed], already resolved through the first-launch
	// exemption (a brand new instance reports open signups).
	Registration string `json:"registration"`
	// EmailVerification reports whether a signup must confirm an emailed code.
	EmailVerification bool `json:"email_verification"`
	// MailDelivers is false when the platform's mail transport writes to a log
	// instead of the wire, so emailed codes never arrive and the operator has
	// to read them from the server.
	MailDelivers bool `json:"mail_delivers"`
	// Passkeys reports whether WebAuthn can work here: it needs a secure
	// context, so a plain-http origin disables it.
	Passkeys bool `json:"passkeys"`
	// Providers are the browser SSO providers this backend can complete a
	// sign-in with (the SSOProvider* values). Native-app token sign-in is
	// separate; see [AuthService.Providers].
	Providers []string `json:"providers"`
	// ProviderLabels is what each provider's button should say, keyed by the
	// same identifiers, so a deployment behind Authentik says so.
	ProviderLabels map[string]string `json:"provider_labels,omitempty"`
	// SelfHosted lets a client drop hosted-only affordances.
	SelfHosted bool `json:"self_hosted"`
	// BillingEnabled is false when the deployment runs without a billing
	// provider: every feature is then unlocked and there is no trial or plan
	// to show. SelfHosted alone does not imply it.
	BillingEnabled bool `json:"billing_enabled"`
	// SetupRequired is true while the instance has no accounts at all; claim
	// it with [AuthService.Setup] before any sign-in can work.
	SetupRequired bool `json:"setup_required"`
	// InvitesRequired mirrors Registration == [RegistrationInviteOnly].
	InvitesRequired bool `json:"invites_required"`
	// DocsURL is where to send someone whose signup was refused by deployment
	// policy.
	DocsURL string `json:"docs_url"`
	// WebsocketURL is the realtime gateway; empty when the instance runs no
	// realtime service. AppURL is the dashboard origin, for building links to
	// pages. Both are served here because on a self-hosted instance the host
	// layout is whatever the operator chose.
	WebsocketURL string `json:"websocket_url,omitempty"`
	AppURL       string `json:"app_url,omitempty"`
}

AuthConfig is what a deployment supports, so one client binary adapts to hosted and self-hosted backends instead of guessing. Everything here is public, non-secret configuration; read it before rendering a sign-in.

type AuthProvider

type AuthProvider struct {
	Enabled  bool   `json:"enabled"`
	ClientID string `json:"client_id,omitempty"`
}

AuthProvider is one social sign-in option. The client id is public.

type AuthProviders

type AuthProviders struct {
	Apple  AuthProvider `json:"apple"`
	Google AuthProvider `json:"google"`
}

AuthProviders reports which social sign-in options the deployment supports, so one client binary adapts to hosted and self-hosted backends.

type AuthService

type AuthService service

AuthService handles user sign-in and the session it produces: the email flow, browser SSO, the CLI device flow, two-factor verification, session management, the caller's own profile, notification preferences and device tokens. It also reads what the deployment supports (AuthService.Config) and, on a self-hosted instance, what version it runs (AuthService.Instance).

This is the credential path for anything an API key cannot reach — workspace governance, billing, the AI assistant. Sign in, then pass the returned Session.AccessToken to WithAccessToken (or wrap it in a TokenSource that refreshes it).

Sign-in is usually two steps: AuthService.Login emails a code and returns an opaque session handle, and AuthService.LoginConfirm exchanges the handle and code for tokens. Whether the code step happens is deployment policy (AuthConfig.LoginCode): with it off, or on a device the account has used before, the first call already carries the tokens, which is why it answers with an AuthStep rather than a bare handle. When the account has 2FA on, whichever call completes the sign-in returns Session.TwoFARequired with a Session.PendingToken to finish through AuthService.VerifyTwoFA.

func (*AuthService) ApproveCLIAuth added in v0.3.0

func (s *AuthService) ApproveCLIAuth(ctx context.Context, userCode, organizationID string, opts ...RequestOption) (*CLIAuthRequest, *Response, error)

ApproveCLIAuth approves a handshake and mints an ordinary API key into the given workspace — not necessarily the session's, because a member of several workspaces picks one on screen. An empty organizationID uses the session's workspace. The key shows up under Settings > API keys and the result's CLIAuthRequest.APIKeyID names it. This route is session-only and needs the manage-API-keys organization permission (OrgPermManageAPIKeys): an API key must not be able to mint another one this way. A code that is no longer pending answers 409.

func (*AuthService) BeginPasskeyLogin

func (s *AuthService) BeginPasskeyLogin(ctx context.Context, opts ...RequestOption) (*PasskeyLoginChallenge, *Response, error)

BeginPasskeyLogin starts a discoverable passkey sign-in and returns the WebAuthn request options together with the handle to finish with. It needs no credentials: there is no account context until the assertion resolves, and the challenge and signature are the protection.

func (*AuthService) BeginPasskeyRegistration

func (s *AuthService) BeginPasskeyRegistration(ctx context.Context, opts ...RequestOption) (json.RawMessage, *Response, error)

BeginPasskeyRegistration starts enrolling a passkey on the signed-in account and returns the WebAuthn creation options.

func (*AuthService) BeginSSO added in v0.3.0

func (s *AuthService) BeginSSO(ctx context.Context, provider string, opts ...RequestOption) (*SSORedirect, *Response, error)

BeginSSO starts a browser sign-in with one of the SSOProvider* providers listed in AuthConfig.Providers and returns the authorization URL to send the browser to. Keep the SSORedirect.Binding: the exchange needs it.

The provider sends the browser back to the API's callback, which redirects to the dashboard's /auth/sso page with a single-use "code" query parameter. Those callbacks are browser redirects, not JSON, so this SDK does not model them; collect the code from the redirect and pass it to AuthService.ExchangeSSO. A refused consent screen redirects back to the login page with no code.

func (*AuthService) CLIAuthRequest added in v0.3.0

func (s *AuthService) CLIAuthRequest(ctx context.Context, userCode string, opts ...RequestOption) (*CLIAuthRequest, *Response, error)

CLIAuthRequest describes a pending handshake by its user code, for an approval screen: which client is asking, from which machine, and for which scopes. This route is session-only.

func (*AuthService) CancelDeletion

func (s *AuthService) CancelDeletion(ctx context.Context, reason string, opts ...RequestOption) (*Response, error)

CancelDeletion cancels a pending account deletion.

func (*AuthService) ChangePassword

func (s *AuthService) ChangePassword(ctx context.Context, currentPassword, newPassword string, opts ...RequestOption) (*Response, error)

ChangePassword sets a new password, which requires the current one.

func (*AuthService) CompleteOnboarding

func (s *AuthService) CompleteOnboarding(ctx context.Context, params *OnboardingParams, opts ...RequestOption) (*User, *Response, error)

CompleteOnboarding answers the first-run questions.

func (*AuthService) Config added in v0.3.0

func (s *AuthService) Config(ctx context.Context, opts ...RequestOption) (*AuthConfig, *Response, error)

Config reports what this deployment supports: which sign-in methods are on, whether a login code follows, whether signups are open, whether the instance still needs claiming. It needs no credentials and is the first call a sign-in screen should make.

func (*AuthService) ConfirmTwoFA

func (s *AuthService) ConfirmTwoFA(ctx context.Context, code string, opts ...RequestOption) (*TwoFARecoveryCodes, *Response, error)

ConfirmTwoFA finishes setup with a code from the authenticator and returns the recovery codes, which are shown exactly once.

func (*AuthService) DangerZone

func (s *AuthService) DangerZone(ctx context.Context, opts ...RequestOption) (*DangerZoneStatus, *Response, error)

DangerZone returns the account's deletion state and the confirmation phrase required to schedule one.

func (*AuthService) DeleteAvatar

func (s *AuthService) DeleteAvatar(ctx context.Context, opts ...RequestOption) (*Response, error)

DeleteAvatar removes the caller's profile picture.

func (*AuthService) DeleteDeviceToken

func (s *AuthService) DeleteDeviceToken(ctx context.Context, token string, opts ...RequestOption) (*Response, error)

DeleteDeviceToken unregisters a device from push notifications.

func (*AuthService) DeletePasskey

func (s *AuthService) DeletePasskey(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeletePasskey removes a registered credential.

func (*AuthService) DenyCLIAuth added in v0.3.0

func (s *AuthService) DenyCLIAuth(ctx context.Context, userCode string, opts ...RequestOption) (*Response, error)

DenyCLIAuth declines a handshake; the polling client sees CLIAuthDenied. Nothing is created, so nothing is audited. This route is session-only and needs OrgPermManageAPIKeys.

func (*AuthService) DisableTwoFA

func (s *AuthService) DisableTwoFA(ctx context.Context, code string, opts ...RequestOption) (*Response, error)

DisableTwoFA turns two-factor off. It requires a current code or a recovery code.

func (*AuthService) EnrollTwoFA

func (s *AuthService) EnrollTwoFA(ctx context.Context, opts ...RequestOption) (*TwoFAEnrollment, *Response, error)

EnrollTwoFA begins two-factor setup and returns the shared secret. Confirm it with AuthService.ConfirmTwoFA before it takes effect.

func (*AuthService) ExchangeSSO added in v0.3.0

func (s *AuthService) ExchangeSSO(ctx context.Context, code, binding string, opts ...RequestOption) (*Session, *Response, error)

ExchangeSSO swaps the handoff code from a provider callback, together with the binding from AuthService.BeginSSO, for a session. The code is single use. A code presented without the binding of the browser that began the sign-in fails with code "sso_wrong_browser", by design: a forwarded handoff link cannot sign its recipient in. Two-factor applies here like everywhere else; see Session.TwoFARequired.

The route is POST /auth/sso/exchange; the server also keeps the older POST /auth/oidc/exchange as an alias for the same handler.

func (*AuthService) FinishPasskeyLogin

func (s *AuthService) FinishPasskeyLogin(ctx context.Context, session string, assertion json.RawMessage, opts ...RequestOption) (*Session, *Response, error)

FinishPasskeyLogin completes a passkey sign-in with the handle from PasskeyLoginChallenge.Session and the authenticator's assertion, and returns the session. It is a single step with no emailed code; two-factor still applies (Session.TwoFARequired).

func (*AuthService) FinishPasskeyRegistration

func (s *AuthService) FinishPasskeyRegistration(ctx context.Context, name string, attestation json.RawMessage, opts ...RequestOption) (*PasskeyCredential, *Response, error)

FinishPasskeyRegistration completes enrollment with the authenticator's attestation and returns the stored credential. Name is the label shown in the credential list; empty lets the server pick one.

func (*AuthService) Instance added in v0.3.0

func (s *AuthService) Instance(ctx context.Context, opts ...RequestOption) (*InstanceInfo, *Response, error)

Instance reports the running version of a self-hosted instance and whether a newer release exists. This route is session-only; any signed-in member may call it. A hosted deployment answers with SelfHosted false and nothing else.

func (*AuthService) Login

func (s *AuthService) Login(ctx context.Context, params *LoginParams, opts ...RequestOption) (*AuthStep, *Response, error)

Login starts a password sign-in. Usually it emails a code and returns the session handle for AuthService.LoginConfirm; when the deployment's login-code policy does not demand one for this device, the returned AuthStep already carries the tokens. Branch on AuthStep.CodeRequired. It is refused when AuthConfig.PasswordLogin is false.

func (*AuthService) LoginConfirm

func (s *AuthService) LoginConfirm(ctx context.Context, params *ConfirmParams, opts ...RequestOption) (*Session, *Response, error)

LoginConfirm exchanges the session handle and emailed code for tokens. When the account has two-factor enabled the result carries Session.TwoFARequired instead.

func (*AuthService) LoginWithApple

func (s *AuthService) LoginWithApple(ctx context.Context, identityToken string, opts ...RequestOption) (*Session, *Response, error)

LoginWithApple exchanges an Apple-signed identity token for a session. The token's signature is the credential, so this needs no prior sign-in.

func (*AuthService) LoginWithGoogle

func (s *AuthService) LoginWithGoogle(ctx context.Context, idToken string, opts ...RequestOption) (*Session, *Response, error)

LoginWithGoogle exchanges a Google-signed ID token for a session.

func (*AuthService) Logout

func (s *AuthService) Logout(ctx context.Context, opts ...RequestOption) (*Response, error)

Logout revokes the current session.

func (*AuthService) LogoutAll

func (s *AuthService) LogoutAll(ctx context.Context, opts ...RequestOption) (*Response, error)

LogoutAll revokes every session on the account, including this one.

func (*AuthService) MarkAllNotificationsRead

func (s *AuthService) MarkAllNotificationsRead(ctx context.Context, opts ...RequestOption) (*Response, error)

MarkAllNotificationsRead marks the whole feed read.

func (*AuthService) MarkNotificationRead

func (s *AuthService) MarkNotificationRead(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

MarkNotificationRead marks one notification read.

func (*AuthService) Me

func (s *AuthService) Me(ctx context.Context, opts ...RequestOption) (*User, *Response, error)

Me returns the caller's own profile, including their folder, tag and category sets.

func (*AuthService) NotificationPreferences

func (s *AuthService) NotificationPreferences(ctx context.Context, opts ...RequestOption) (*NotificationPreferencesResult, *Response, error)

NotificationPreferences returns the caller's notification configuration.

func (*AuthService) Notifications

func (s *AuthService) Notifications(ctx context.Context, opts ...RequestOption) (*NotificationFeed, *Response, error)

Notifications returns the caller's in-app feed and unread count, newest first. The feed is capped at 50 entries unless a "limit" is given, and "unread=true" narrows it to what has not been read:

feed, _, err := client.Auth.Notifications(ctx,
	warmbly.WithQueryParam("unread", "true"))

func (*AuthService) Passkeys

func (s *AuthService) Passkeys(ctx context.Context, opts ...RequestOption) ([]PasskeyCredential, *Response, error)

Passkeys returns the account's registered WebAuthn credentials.

func (*AuthService) PollCLIAuth added in v0.3.0

func (s *AuthService) PollCLIAuth(ctx context.Context, deviceCode string, opts ...RequestOption) (*CLIAuthPoll, *Response, error)

PollCLIAuth asks once whether a member has decided. The answer is CLIAuthPending until they do, then CLIAuthApproved carrying the minted key exactly once (a later poll finds the code spent), or CLIAuthDenied. An unknown or expired device code is a 404 (ErrNotFound): the two are indistinguishable on purpose, so a poller cannot probe for live handshakes. Respect CLIAuthHandshake.Interval between polls; polling faster is rate limited per IP.

func (*AuthService) Providers

func (s *AuthService) Providers(ctx context.Context, opts ...RequestOption) (*AuthProviders, *Response, error)

Providers reports which social sign-in options this deployment supports. It needs no credentials.

func (*AuthService) Refresh

func (s *AuthService) Refresh(ctx context.Context, refreshToken string, opts ...RequestOption) (*Session, *Response, error)

Refresh exchanges a refresh token for a fresh token pair. See [RefreshTokenSource] to have the client do this automatically.

func (*AuthService) Register

func (s *AuthService) Register(ctx context.Context, params *LoginParams, opts ...RequestOption) (*AuthStep, *Response, error)

Register starts account creation. With email verification on it emails a code and returns the handle for AuthService.RegisterConfirm; otherwise the account is created at once and the AuthStep carries its session. On an invite-only deployment LoginParams.Invite is required; see the codes it documents.

func (*AuthService) RegisterConfirm

func (s *AuthService) RegisterConfirm(ctx context.Context, params *ConfirmParams, opts ...RequestOption) (*AuthStep, *Response, error)

RegisterConfirm completes account creation with the emailed code and signs the new account in: the result's AuthStep.Token is the session. It is nil only when the account was created but could not be signed in on the spot, in which case AuthService.Login works.

func (*AuthService) RegisterDeviceToken

func (s *AuthService) RegisterDeviceToken(ctx context.Context, token, platform, environment string, opts ...RequestOption) (*Response, error)

RegisterDeviceToken registers a device for push notifications. The token is an APNs token (hex); platform must be "ios" or empty, and environment "production", "development" or empty, which defaults to production. Registering the same token again is harmless: it refreshes the record rather than creating a second one. The registration is not readable back — there is no route that lists a user's devices.

func (*AuthService) RenamePasskey

func (s *AuthService) RenamePasskey(ctx context.Context, id, name string, opts ...RequestOption) (*PasskeyCredential, *Response, error)

RenamePasskey relabels a registered credential.

func (*AuthService) ResetPassword

func (s *AuthService) ResetPassword(ctx context.Context, email, turnstile string, opts ...RequestOption) (*Response, error)

ResetPassword emails a password-reset code. It answers the same way whether or not the address has an account.

func (*AuthService) ResetPasswordConfirm

func (s *AuthService) ResetPasswordConfirm(ctx context.Context, session, password, turnstile string, opts ...RequestOption) (*Response, error)

ResetPasswordConfirm sets a new password using the emailed session handle.

func (*AuthService) RevokeOtherSessions

func (s *AuthService) RevokeOtherSessions(ctx context.Context, opts ...RequestOption) (*Response, error)

RevokeOtherSessions signs out every session except the current one.

func (*AuthService) RevokeSession

func (s *AuthService) RevokeSession(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

RevokeSession signs one session out.

func (*AuthService) ScheduleDeletion

func (s *AuthService) ScheduleDeletion(ctx context.Context, params *ScheduleDeletionParams, opts ...RequestOption) (*ScheduledDeletion, *Response, error)

ScheduleDeletion schedules the caller's own account for a delayed hard delete. Confirmation must match DangerZoneStatus.ConfirmationHint, which for an account is their email address.

func (*AuthService) Sessions

func (s *AuthService) Sessions(ctx context.Context, opts ...RequestOption) ([]UserSession, *Response, error)

Sessions returns the account's active sessions.

func (*AuthService) SetUndoSendSeconds

func (s *AuthService) SetUndoSendSeconds(ctx context.Context, seconds int, opts ...RequestOption) (*Response, error)

SetUndoSendSeconds sets how long an instant send is held, and stays cancelable, before it leaves. The server bounds the value.

func (*AuthService) Setup added in v0.3.0

func (s *AuthService) Setup(ctx context.Context, params *SetupParams, opts ...RequestOption) (*Session, *Response, error)

Setup claims a fresh self-hosted instance: it exchanges the one-time setup token for the owner account and returns its session, so there is no second sign-in. It needs no credentials — the token is the protection — and works only while AuthConfig.SetupRequired is true. An invalid, used or expired token fails with code "setup_token_invalid"; an instance that already has an account fails with "setup_already_complete".

func (*AuthService) StartCLIAuth added in v0.3.0

func (s *AuthService) StartCLIAuth(ctx context.Context, params *CLIAuthStartParams, opts ...RequestOption) (*CLIAuthHandshake, *Response, error)

StartCLIAuth opens a device-flow handshake for a client that holds no credential yet. Show the person CLIAuthHandshake.UserCode, open CLIAuthHandshake.VerificationURIComplete, then poll with AuthService.PollCLIAuth or let AuthService.WaitForCLIAuth do it. It needs no credentials and is per-IP rate limited; the handshake expires after CLIAuthHandshake.ExpiresIn seconds. A deployment without CLI sign-in answers 501.

func (*AuthService) TwoFAStatus

func (s *AuthService) TwoFAStatus(ctx context.Context, opts ...RequestOption) (*TwoFAStatus, *Response, error)

TwoFAStatus reports whether two-factor is enabled on the account.

func (*AuthService) UpdateNotificationPreferences

func (s *AuthService) UpdateNotificationPreferences(ctx context.Context, prefs *NotificationPreferences, opts ...RequestOption) (*NotificationPreferencesResult, *Response, error)

UpdateNotificationPreferences replaces the caller's notification configuration.

func (*AuthService) UpdateProfile

func (s *AuthService) UpdateProfile(ctx context.Context, params *ProfileUpdateParams, opts ...RequestOption) (*User, *Response, error)

UpdateProfile changes the caller's name.

func (*AuthService) UploadAvatar

func (s *AuthService) UploadAvatar(ctx context.Context, file *FileUpload, opts ...RequestOption) (string, *Response, error)

UploadAvatar sets the caller's profile picture and returns the URL it is now served from. The image must be a PNG or JPEG, at most 2 MB and 1024 pixels on a side; anything else is refused. The previous avatar is deleted.

func (*AuthService) VerifyTwoFA

func (s *AuthService) VerifyTwoFA(ctx context.Context, pendingToken, code string, opts ...RequestOption) (*Session, *Response, error)

VerifyTwoFA completes a sign-in that stopped at the two-factor step, using the pending token from AuthService.LoginConfirm and a TOTP or recovery code.

func (*AuthService) WaitForCLIAuth added in v0.3.0

func (s *AuthService) WaitForCLIAuth(ctx context.Context, hs *CLIAuthHandshake, opts ...RequestOption) (*CLIAuthPoll, error)

WaitForCLIAuth polls a handshake until a member decides, the handshake expires, or ctx is done. It sleeps CLIAuthHandshake.Interval between polls (five seconds when the server sent none) and, when the server asks it to slow down with a 429, waits the Retry-After it was given — or five more seconds — before continuing rather than failing.

On approval it returns the poll carrying the key. A denial returns ErrCLIAuthDenied; an expired handshake surfaces as ErrNotFound from the poll; and a canceled context returns its error. Typical use:

hs, _, err := client.Auth.StartCLIAuth(ctx, &warmbly.CLIAuthStartParams{Hostname: host})
fmt.Println("Approve at", hs.VerificationURIComplete)
grant, err := client.Auth.WaitForCLIAuth(ctx, hs)
// grant.Token is the API key

type AuthStep added in v0.3.0

type AuthStep struct {
	// Session is the opaque handle for the confirm step. Empty when the flow
	// already finished.
	Session string `json:"session,omitempty"`
	// CodeRequired is true when an emailed code must be confirmed next.
	CodeRequired bool `json:"code_required"`
	// Token is the session when the flow finished in one step. It is nil while
	// a code is still required, and also nil after a completed signup that
	// could not be signed in immediately: the account exists, so sign in with
	// [AuthService.Login].
	Token *Session `json:"token,omitempty"`

	// TwoFARequired, PendingToken and ExpiresIn are the 2FA challenge, set
	// instead of Token when the flow finished but the account has two-factor
	// on. Finish with [AuthService.VerifyTwoFA].
	TwoFARequired bool   `json:"two_fa_required,omitempty"`
	PendingToken  string `json:"pending_token,omitempty"`
	ExpiresIn     int    `json:"expires_in,omitempty"`
}

AuthStep is the outcome of the first step of a sign-in or signup. Either another step is needed — CodeRequired is true and Session is the handle to pass to the confirm call — or the flow finished in one call and Token holds the session (with the usual 2FA challenge fields when the account has two-factor on).

A deployment decides which: an emailed login code can be off entirely, or skipped on a device the account has signed in from before; a signup skips verification when the deployment does not require it or cannot deliver mail. Branch on CodeRequired rather than assuming the code step.

func (*AuthStep) Done added in v0.3.0

func (a *AuthStep) Done() bool

Done reports whether the flow finished in this step, with either a session or a 2FA challenge to complete.

type Authenticator

type Authenticator interface {
	// contains filtered or unexported methods
}

Authenticator attaches credentials to an outgoing request. The SDK ships with API-key and OAuth access-token authenticators; you may also supply your own via WithAuthenticator.

type AuthenticatorFunc

type AuthenticatorFunc func(req *http.Request) error

AuthenticatorFunc adapts an ordinary function to an Authenticator.

type AuthorizeParams

type AuthorizeParams struct {
	// ResponseType is "code".
	ResponseType string `json:"response_type"`
	ClientID     string `json:"client_id"`
	RedirectURI  string `json:"redirect_uri"`
	// Scope is the space-delimited set being granted.
	Scope string `json:"scope"`
	State string `json:"state"`
	// CodeChallenge and CodeChallengeMethod carry PKCE, which is required for
	// a public client.
	CodeChallenge       string `json:"code_challenge,omitempty"`
	CodeChallengeMethod string `json:"code_challenge_method,omitempty"`
}

AuthorizeParams approves a consent request and mints an authorization code. The fields mirror the query parameters the application sent the user with.

type AuthorizedApp

type AuthorizedApp struct {
	ApplicationID string `json:"application_id"`
	Name          string `json:"name"`
	LogoURL       string `json:"logo_url,omitempty"`
	WebsiteURL    string `json:"website_url,omitempty"`
	// Scopes is what was actually granted, which may be less than the app
	// asked for.
	Scopes       uint64     `json:"scopes"`
	AuthorizedAt time.Time  `json:"authorized_at"`
	LastUsedAt   *time.Time `json:"last_used_at,omitempty"`
}

AuthorizedApp is an application a user has granted access to their workspace.

type Automation

type Automation struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	Name           string `json:"name"`
	Enabled        bool   `json:"enabled"`
	// TriggerEvent is the event key that starts the run: a Trigger* constant
	// or any Event* constant.
	TriggerEvent string `json:"trigger_event"`
	// Filter narrows which occurrences of the trigger event actually run it.
	Filter json.RawMessage `json:"filter,omitempty"`
	Graph  AutomationGraph `json:"graph"`
	// InboundURL is the public POST URL that fires the automation, set only
	// when TriggerEvent is [TriggerInboundWebhook]. It embeds a per-automation
	// secret, so treat it as a credential.
	InboundURL string `json:"inbound_url,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Automation is a trigger plus the graph that runs when it fires.

type AutomationCondition

type AutomationCondition struct {
	// Field is one of the Condition* constants.
	Field string `json:"field"`
	// Key names the event-data key to read for [ConditionField].
	Key string `json:"key,omitempty"`
	// Operator is one of the ConditionOp* constants.
	Operator string `json:"operator"`
	Value    any    `json:"value,omitempty"`
	// Expression is the Go-template predicate for [ConditionExpression].
	Expression string `json:"expression,omitempty"`
	// Prompt is the yes/no question for [ConditionAI].
	Prompt string `json:"prompt,omitempty"`
}

AutomationCondition is a condition node's test. Field picks the kind of test; the other fields apply according to the Condition* constant.

type AutomationCreateParams

type AutomationCreateParams struct {
	Name    string `json:"name"`
	Enabled bool   `json:"enabled"`
	// TriggerEvent is the event key that starts it.
	TriggerEvent string          `json:"trigger_event"`
	Filter       json.RawMessage `json:"filter,omitempty"`
	Graph        AutomationGraph `json:"graph"`
}

AutomationCreateParams creates an automation.

type AutomationEdge

type AutomationEdge struct {
	ID     string `json:"id"`
	Source string `json:"source"`
	Target string `json:"target"`
	When   string `json:"when,omitempty"`
}

AutomationEdge connects two nodes. When is the branch label; see the EdgeWhen* constants. The graph may not contain a cycle: a flow that loops back on itself is a 400 when saved.

type AutomationGraph

type AutomationGraph struct {
	Nodes []AutomationNode `json:"nodes"`
	Edges []AutomationEdge `json:"edges"`
}

AutomationGraph is the node graph, as drawn on the builder canvas.

type AutomationNode

type AutomationNode struct {
	ID string `json:"id"`
	// Type is [NodeTrigger], [NodeCondition], [NodeAction] or [NodeStop].
	Type string `json:"type"`
	// Action is the action identifier for an action node; see the Action*
	// constants.
	Action string `json:"action,omitempty"`
	// ConnectionID is the integration a provider action runs against. The
	// built-in "warmbly." actions leave it nil.
	ConnectionID *string `json:"connection_id,omitempty"`
	// Config is the action's parameters. Its shape depends on Action; the
	// built-in [UpsertContactConfig] and [AddToCampaignConfig] render into it,
	// and the server validates it when the automation is saved (400).
	Config json.RawMessage `json:"config,omitempty"`
	// Condition is the test a condition node applies.
	Condition *AutomationCondition `json:"condition,omitempty"`
	// X and Y are the node's canvas coordinates.
	X float64 `json:"x"`
	Y float64 `json:"y"`
}

AutomationNode is one step. Which fields matter depends on Type.

type AutomationNodeResult

type AutomationNodeResult struct {
	NodeID string `json:"node_id"`
	// Type is [NodeTrigger], [NodeCondition] or [NodeAction].
	Type   string `json:"type"`
	Action string `json:"action,omitempty"`
	Label  string `json:"label,omitempty"`
	// Status is one of the NodeStatus* constants.
	Status string `json:"status"`
	Error  string `json:"error,omitempty"`
	// Preview is a JSON object of what the action sent or would send: the
	// rendered templates of a dry run, plus run-time output such as the
	// contact_id and contact_created of an [ActionUpsertContact] node.
	Preview json.RawMessage `json:"preview,omitempty"`
}

AutomationNodeResult is what one node did during a run or a dry run.

type AutomationRun

type AutomationRun struct {
	ID             string `json:"id"`
	AutomationID   string `json:"automation_id"`
	OrganizationID string `json:"organization_id,omitempty"`
	TriggerEvent   string `json:"trigger_event"`
	// Status is [AutomationRunning], [AutomationSuccess] or [AutomationError].
	Status      string                 `json:"status"`
	NodeResults []AutomationNodeResult `json:"node_results"`
	ErrorDetail string                 `json:"error_detail,omitempty"`
	StartedAt   time.Time              `json:"started_at"`
	FinishedAt  *time.Time             `json:"finished_at,omitempty"`
}

AutomationRun is one execution, with a per-node trace.

type AutomationService

type AutomationService service

AutomationService manages automations: a trigger event plus a graph of condition and action nodes that runs across connected integrations and the built-in Warmbly actions.

An automation can also be triggered from outside Warmbly. Each one whose trigger is TriggerInboundWebhook gets a token-addressed inbound URL (Automation.InboundURL) that runs it with the POSTed JSON body as the event payload.

Chains are bounded. Everything an action does (a contact it creates, a deal it opens, an automation it launches) fires events one hop deeper, and past MaxAutomationDepth hops those events still reach webhooks but run no more automations, so "on contact created, create a contact" cannot loop.

func (*AutomationService) Create

Create creates an automation. Node configs are validated, so a built-in action missing its required field fails here with a 400.

func (*AutomationService) Delete

func (s *AutomationService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes an automation. It fails with a 409 while a campaign step still runs it; remove the step first.

func (*AutomationService) DryRun added in v0.3.0

DryRun tests an automation without side effects. Conditions evaluate for real and action nodes are previewed rather than executed, with one exception: AI nodes run for real, and are charged, so the trace shows the model's actual output.

func (*AutomationService) Get

Get retrieves a single automation, including its graph.

func (*AutomationService) List

List returns the workspace's automations.

func (*AutomationService) Runs

func (s *AutomationService) Runs(ctx context.Context, id string, limit int, opts ...RequestOption) ([]AutomationRun, *Response, error)

Runs returns an automation's recent runs, newest first. A limit of 0 uses the server default of 50.

func (*AutomationService) Test

Test dry-runs an automation against sample event data and returns the trace. Pass nil data to use the server's sample for the trigger. It is AutomationService.DryRun without the option to skip nodes.

func (*AutomationService) Update

Update replaces an automation's definition. See AutomationUpdateParams.

func (*AutomationService) UpdateLayout

func (s *AutomationService) UpdateLayout(ctx context.Context, id string, positions []NodePosition, opts ...RequestOption) (*Response, error)

UpdateLayout persists node coordinates only. It is cosmetic: unaudited, last-write-wins, and safe to call continuously as nodes are dragged. At most 1,000 positions are accepted per call.

type AutomationTestParams added in v0.3.0

type AutomationTestParams struct {
	// Data is the sample event payload. Leave it nil to let the server build
	// one from the trigger.
	Data any `json:"data,omitempty"`
	// SkipNodeIDs are action nodes to leave out of this test. They are
	// recorded as [NodeStatusSkipped] and never previewed, but the walk still
	// follows their edges so later steps are shown.
	SkipNodeIDs []string `json:"skip_node_ids,omitempty"`
}

AutomationTestParams configures a dry run.

type AutomationTestResult

type AutomationTestResult struct {
	Trace []AutomationNodeResult `json:"trace"`
	Data  json.RawMessage        `json:"data"`
}

AutomationTestResult is a dry run: the node-by-node trace plus the event data it ran against, including anything the nodes wrote into it.

type AutomationUpdateParams

type AutomationUpdateParams = AutomationCreateParams

AutomationUpdateParams replaces an automation's definition. Unlike most update params these are not merged: the API rewrites the name, enabled flag, trigger, filter and node graph from what you send, so send the complete desired state. Read the current one with AutomationService.Get first if you only mean to change part of it.

type BillingService

type BillingService service

BillingService reads and changes the workspace's subscription, plan, AI credit balance and spend controls, and the referral program.

These routes are session-only and gated on the manage-billing permission, so they need a session token rather than an API key. On a self-hosted deployment with billing disabled they may be absent entirely.

func (*BillingService) AppliedDiscounts

func (s *BillingService) AppliedDiscounts(ctx context.Context, opts ...RequestOption) ([]DiscountRedemption, *Response, error)

AppliedDiscounts returns the promotions redeemed on the workspace, most recent first. It is not paginated: the server answers with its most recent 50.

func (*BillingService) BuyCredits

func (s *BillingService) BuyCredits(ctx context.Context, pack, successURL, cancelURL string, opts ...RequestOption) (*CheckoutSession, *Response, error)

BuyCredits starts a hosted checkout for a top-up pack (a CreditPack.Key from CreditBalance.Packs). It requires an active paid plan; all three arguments are required.

func (*BillingService) Cancel

func (s *BillingService) Cancel(ctx context.Context, atPeriodEnd bool, opts ...RequestOption) (*Response, error)

Cancel schedules the subscription to lapse at the end of the current period (atPeriodEnd true), or clears a cancellation that was already scheduled so it renews again (atPeriodEnd false). Either way the workspace keeps its plan until the period actually ends. It needs an active provider subscription; a workspace that never had one is refused.

func (*BillingService) ChangePlan

func (s *BillingService) ChangePlan(ctx context.Context, params *ChangePlanParams, opts ...RequestOption) (*Subscription, *Response, error)

ChangePlan moves the workspace to a different plan and returns the updated subscription. It needs the manage-billing organization permission.

func (*BillingService) Checkout

func (s *BillingService) Checkout(ctx context.Context, params *CheckoutParams, opts ...RequestOption) (*CheckoutSession, *Response, error)

Checkout starts a hosted checkout for a plan and returns the URL to send the user to.

func (*BillingService) CreditSettings

func (s *BillingService) CreditSettings(ctx context.Context, opts ...RequestOption) (*CreditSettings, *Response, error)

CreditSettings returns the workspace's AI spend controls.

func (*BillingService) CreditTransactions

func (s *BillingService) CreditTransactions(ctx context.Context, params *ListOptions, opts ...RequestOption) (*Page[CreditTransaction], error)

CreditTransactions returns a page of the credit ledger, newest first.

func (*BillingService) CreditUsage

func (s *BillingService) CreditUsage(ctx context.Context, days int, opts ...RequestOption) (*CreditUsage, *Response, error)

CreditUsage returns AI spend over the last days, from 1 to 90. Zero uses the server default.

func (*BillingService) Credits

func (s *BillingService) Credits(ctx context.Context, opts ...RequestOption) (*CreditBalance, *Response, error)

Credits returns the workspace's AI credit position. Check CreditBalance.Unlimited first: a deployment without billing meters nothing.

func (*BillingService) EnsureReferralCode

func (s *BillingService) EnsureReferralCode(ctx context.Context, opts ...RequestOption) (*ReferralCode, *Response, error)

EnsureReferralCode mints the workspace's referral code if it does not have one yet, and returns it either way. It is idempotent: a workspace has one code, and calling this again returns the same one. For the shareable link and the running totals, read BillingService.Referral.

func (*BillingService) EnterpriseInquiry

func (s *BillingService) EnterpriseInquiry(ctx context.Context, params *EnterpriseInquiryParams, opts ...RequestOption) (string, *Response, error)

EnterpriseInquiry asks the sales team to get in touch about enterprise pricing and returns the inquiry's id.

func (*BillingService) Features

func (s *BillingService) Features(ctx context.Context, opts ...RequestOption) (*FeatureStatus, *Response, error)

Features returns the capability gates a client should check before offering an action.

func (*BillingService) Get

Get returns the workspace's subscription.

func (*BillingService) Limits

Limits returns the subscription together with the realtime rate limits its plan carries. For the plan-gate view (trial state, daily send cap) see BillingService.Features.

func (*BillingService) Portal

func (s *BillingService) Portal(ctx context.Context, returnURL string, opts ...RequestOption) (string, *Response, error)

Portal opens the payment provider's billing portal and returns its URL. returnURL, where the portal sends the user back, is required.

func (*BillingService) PreviewPlanChange

func (s *BillingService) PreviewPlanChange(ctx context.Context, newPlanID string, opts ...RequestOption) (*PlanChangePreview, *Response, error)

PreviewPlanChange returns the proration moving to newPlanID would produce, without making the change. It needs the manage-billing organization permission.

func (*BillingService) Referral

func (s *BillingService) Referral(ctx context.Context, opts ...RequestOption) (*ReferralSummary, *Response, error)

Referral returns the workspace's referral summary.

func (*BillingService) ReferralAttributions

func (s *BillingService) ReferralAttributions(ctx context.Context, params *ListOptions, opts ...RequestOption) (*Page[ReferralAttribution], error)

ReferralAttributions returns a page of the workspaces referred, with where each reward stands.

func (*BillingService) ReferralEarnings

func (s *BillingService) ReferralEarnings(ctx context.Context, params *ListOptions, opts ...RequestOption) (*Page[ReferralEarning], error)

ReferralEarnings returns a page of the referral credit ledger.

func (*BillingService) Trial

func (s *BillingService) Trial(ctx context.Context, opts ...RequestOption) (*TrialStatus, *Response, error)

Trial returns where the workspace stands in its free trial.

func (*BillingService) UpdateCreditSettings

func (s *BillingService) UpdateCreditSettings(ctx context.Context, params *CreditSettingsParams, opts ...RequestOption) (*CreditSettings, *Response, error)

UpdateCreditSettings saves the AI spend controls.

func (*BillingService) ValidateDiscount

func (s *BillingService) ValidateDiscount(ctx context.Context, code, planID string, opts ...RequestOption) (*DiscountPreview, *Response, error)

ValidateDiscount checks a promotion code and reports what it would do. Name a planID to get the amounts it works out to on that plan; pass "" to just check the code. A code that cannot be used is not an error: the preview comes back with DiscountPreview.Valid false and a reason.

type BouncePipelineSettings

type BouncePipelineSettings struct {
	Enabled                   bool `json:"enabled"`
	AutoSuppressOnBounce      bool `json:"auto_suppress_on_bounce"`
	AutoSuppressOnComplaint   bool `json:"auto_suppress_on_complaint"`
	AutoSuppressOnUnsubscribe bool `json:"auto_suppress_on_unsubscribe"`
	AutoPauseCampaignOnSpike  bool `json:"auto_pause_campaign_on_spike"`
	// PauseBounceRateThreshold and PauseComplaintRateThreshold are rates in
	// [0,1] at which a campaign is paused.
	PauseBounceRateThreshold    float64 `json:"pause_bounce_rate_threshold"`
	PauseComplaintRateThreshold float64 `json:"pause_complaint_rate_threshold"`
}

BouncePipelineSettings controls automatic suppression and the circuit breaker that pauses a campaign when bounces or complaints spike.

type BulkTagParams

type BulkTagParams struct {
	EmailIDs   []string `json:"email_ids"`
	AddTags    []string `json:"add_tags,omitempty"`
	RemoveTags []string `json:"remove_tags,omitempty"`
}

BulkTagParams adds and removes tags across many mailboxes at once. Mailbox and tag ids the caller cannot see are ignored rather than failing the batch, and the operation is set-based, so retries are naturally safe.

type BulkTagResult

type BulkTagResult struct {
	Updated int `json:"updated"`
}

BulkTagResult reports how many mailboxes the tag change touched.

type CLIAuthHandshake added in v0.3.0

type CLIAuthHandshake struct {
	// DeviceCode is the secret this client keeps and polls with.
	DeviceCode string `json:"device_code"`
	// UserCode is what to show the person: eight unambiguous characters they
	// match against the approval screen.
	UserCode string `json:"user_code"`
	// VerificationURI is the approval page; VerificationURIComplete carries
	// the code already, so opening it needs no typing.
	VerificationURI         string `json:"verification_uri"`
	VerificationURIComplete string `json:"verification_uri_complete"`
	// ExpiresIn is the handshake's lifetime in seconds; Interval is how many
	// seconds to wait between polls.
	ExpiresIn int `json:"expires_in"`
	Interval  int `json:"interval"`
}

CLIAuthHandshake is an open device-flow handshake, shaped like RFC 8628 so a generic device-flow client works against it.

type CLIAuthPoll added in v0.3.0

type CLIAuthPoll struct {
	// Status is [CLIAuthPending], [CLIAuthApproved] or [CLIAuthDenied].
	Status string `json:"status"`
	// Token is the minted API key (prefixed "wmbly_"). It is shown here and
	// never again: store it before returning.
	Token string `json:"token,omitempty"`
	// APIKeyID identifies the key under Settings > API keys, which is where
	// it can be revoked (or with [APIKeyService.RevokeSelf]).
	APIKeyID *string `json:"api_key_id,omitempty"`
	// Scopes is the granted permission mask; ScopeNames the same as names.
	Scopes     uint64   `json:"scopes,omitempty"`
	ScopeNames []string `json:"scope_names,omitempty"`
	// The signed-in identity, for labeling the credential.
	UserID           *string `json:"user_id,omitempty"`
	UserEmail        string  `json:"user_email,omitempty"`
	UserName         string  `json:"user_name,omitempty"`
	OrganizationID   *string `json:"organization_id,omitempty"`
	OrganizationName string  `json:"organization_name,omitempty"`
}

CLIAuthPoll answers one poll. Status is the only field always set; the key fields arrive exactly once, on the poll that finds the code approved.

type CLIAuthRequest added in v0.3.0

type CLIAuthRequest struct {
	ID         string   `json:"id"`
	UserCode   string   `json:"user_code"`
	ClientName string   `json:"client_name"`
	Hostname   string   `json:"hostname"`
	CLIVersion string   `json:"cli_version"`
	Scopes     uint64   `json:"scopes"`
	ScopeNames []string `json:"scope_names"`
	// Status is one of the CLIAuth* constants.
	Status string `json:"status"`
	// OrganizationID is the workspace the key was minted in, once approved.
	OrganizationID *string `json:"organization_id,omitempty"`
	// APIKeyID is set on the approval response only: the key that was minted.
	APIKeyID  *string   `json:"api_key_id,omitempty"`
	ExpiresAt time.Time `json:"expires_at"`
	CreatedAt time.Time `json:"created_at"`
}

CLIAuthRequest is what the approving member is shown before deciding: who is asking, from where, and for which scopes.

type CLIAuthStartParams added in v0.3.0

type CLIAuthStartParams struct {
	ClientName string `json:"client_name,omitempty"`
	Hostname   string `json:"hostname,omitempty"`
	CLIVersion string `json:"cli_version,omitempty"`
	Scopes     uint64 `json:"scopes,omitempty"`
}

CLIAuthStartParams opens a device-flow handshake. Everything but Scopes is display-only: it is what the approving member sees. Scopes is the API permission mask the minted key will hold (the Perm* constants); zero asks for the server default.

type CRMService

type CRMService service

CRMService manages the built-in CRM: deal pipelines and their stages, deals, and the task board that hangs off contacts and deals.

The deal and task lists come in two shapes. The plain List methods are cursor pages for straightforward browsing; the Search methods take a faceted filter body and are aggregated server-side, so a total or a per-stage sum is a true count over the whole matching set rather than a reduce over one page.

func (*CRMService) CreateDeal

func (s *CRMService) CreateDeal(ctx context.Context, params *DealCreateParams, opts ...RequestOption) (*Deal, *Response, error)

CreateDeal opens a new deal.

func (*CRMService) CreatePipeline

func (s *CRMService) CreatePipeline(ctx context.Context, params *PipelineCreateParams, opts ...RequestOption) (*Pipeline, *Response, error)

CreatePipeline creates a pipeline, optionally seeding its stages.

func (*CRMService) CreateStage

func (s *CRMService) CreateStage(ctx context.Context, pipelineID string, params *StageCreateParams, opts ...RequestOption) (*PipelineStage, *Response, error)

CreateStage appends a stage to a pipeline.

func (*CRMService) CreateTask

func (s *CRMService) CreateTask(ctx context.Context, params *CRMTaskCreateParams, opts ...RequestOption) (*CRMTask, *Response, error)

CreateTask opens a new CRM task.

func (*CRMService) CreateTaskType

func (s *CRMService) CreateTaskType(ctx context.Context, params *TaskTypeCreateParams, opts ...RequestOption) (*CRMTaskType, *Response, error)

CreateTaskType adds a task type.

func (*CRMService) DealsSummary

func (s *CRMService) DealsSummary(ctx context.Context, params *DealSearchParams, opts ...RequestOption) (*DealsSummary, *Response, error)

DealsSummary aggregates every deal matching the same filter a search takes.

func (*CRMService) DeleteDeal

func (s *CRMService) DeleteDeal(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteDeal removes a deal.

func (*CRMService) DeletePipeline

func (s *CRMService) DeletePipeline(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeletePipeline removes a pipeline.

func (*CRMService) DeleteStage

func (s *CRMService) DeleteStage(ctx context.Context, pipelineID, stageID string, opts ...RequestOption) (*Response, error)

DeleteStage removes a pipeline stage.

func (*CRMService) DeleteTask

func (s *CRMService) DeleteTask(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteTask removes a CRM task.

func (*CRMService) DeleteTaskType

func (s *CRMService) DeleteTaskType(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteTaskType removes a task type. Tasks that used it keep its name.

func (*CRMService) GetDeal

func (s *CRMService) GetDeal(ctx context.Context, id string, opts ...RequestOption) (*Deal, *Response, error)

GetDeal retrieves a single deal.

func (*CRMService) GetPipeline

func (s *CRMService) GetPipeline(ctx context.Context, id string, opts ...RequestOption) (*Pipeline, *Response, error)

GetPipeline retrieves a pipeline and its stages.

func (*CRMService) GetTask

func (s *CRMService) GetTask(ctx context.Context, id string, opts ...RequestOption) (*CRMTask, *Response, error)

GetTask retrieves a single CRM task.

func (*CRMService) ListDeals

func (s *CRMService) ListDeals(ctx context.Context, params *DealListParams, opts ...RequestOption) (*Page[Deal], error)

ListDeals returns a page of deals, optionally narrowed to one pipeline, stage or status. Rows carry their joined contact and stage.

func (*CRMService) ListPipelines

func (s *CRMService) ListPipelines(ctx context.Context, opts ...RequestOption) ([]Pipeline, *Response, error)

ListPipelines returns the organization's pipelines with their stages.

func (*CRMService) ListTaskTypes

func (s *CRMService) ListTaskTypes(ctx context.Context, opts ...RequestOption) ([]CRMTaskType, *Response, error)

ListTaskTypes returns the organization's task types. The first read seeds a usable default set.

func (*CRMService) ListTasks

func (s *CRMService) ListTasks(ctx context.Context, params *CRMTaskListParams, opts ...RequestOption) (*Page[CRMTask], error)

ListTasks returns a page of CRM tasks, optionally narrowed to one contact, deal, assignee or status.

func (*CRMService) SearchDeals

func (s *CRMService) SearchDeals(ctx context.Context, params *DealSearchParams, opts ...RequestOption) (*Page[Deal], error)

SearchDeals returns a page of deals matching a faceted filter. Its page limit is capped at 200, twice what CRMService.ListDeals allows, and Pagination.Total is an exact count over the whole matching set.

func (*CRMService) SearchTasks

func (s *CRMService) SearchTasks(ctx context.Context, params *TaskSearchParams, opts ...RequestOption) (*Page[CRMTask], error)

SearchTasks returns a page of tasks matching a faceted filter. Its page limit is capped at 200, twice what CRMService.ListTasks allows, and Pagination.Total is an exact count over the whole matching set.

func (*CRMService) TasksSummary

func (s *CRMService) TasksSummary(ctx context.Context, params *TaskSearchParams, opts ...RequestOption) (*TasksSummary, *Response, error)

TasksSummary aggregates every task matching the same filter a search takes.

func (*CRMService) UpdateDeal

func (s *CRMService) UpdateDeal(ctx context.Context, id string, params *DealUpdateParams, opts ...RequestOption) (*Deal, *Response, error)

UpdateDeal modifies a deal, including moving it between stages or closing it.

func (*CRMService) UpdatePipeline

func (s *CRMService) UpdatePipeline(ctx context.Context, id string, params *PipelineUpdateParams, opts ...RequestOption) (*Pipeline, *Response, error)

UpdatePipeline renames a pipeline.

func (*CRMService) UpdateStage

func (s *CRMService) UpdateStage(ctx context.Context, pipelineID, stageID string, params *StageUpdateParams, opts ...RequestOption) (*PipelineStage, *Response, error)

UpdateStage modifies a pipeline stage.

func (*CRMService) UpdateTask

func (s *CRMService) UpdateTask(ctx context.Context, id string, params *CRMTaskUpdateParams, opts ...RequestOption) (*CRMTask, *Response, error)

UpdateTask modifies a CRM task.

func (*CRMService) UpdateTaskType

func (s *CRMService) UpdateTaskType(ctx context.Context, id string, params *TaskTypeUpdateParams, opts ...RequestOption) (*CRMTaskType, *Response, error)

UpdateTaskType modifies a task type.

type CRMTask

type CRMTask struct {
	ID             string  `json:"id"`
	OrganizationID string  `json:"organization_id"`
	ContactID      *string `json:"contact_id,omitempty"`
	DealID         *string `json:"deal_id,omitempty"`
	AssignedTo     *string `json:"assigned_to,omitempty"`
	AssignedTeamID *string `json:"assigned_team_id,omitempty"`
	CreatedBy      string  `json:"created_by"`

	Title       string     `json:"title"`
	Description *string    `json:"description,omitempty"`
	DueDate     *time.Time `json:"due_date,omitempty"`
	// Priority is one of the TaskPriority* constants.
	Priority string `json:"priority"`
	// Type is a [CRMTaskType] name.
	Type string `json:"type"`
	// Status is one of the TaskStatus* constants.
	Status      string     `json:"status"`
	CompletedAt *time.Time `json:"completed_at,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

CRMTask is a unit of follow-up work, optionally attached to a contact or a deal and assigned to a member or a whole team.

type CRMTaskCreateParams

type CRMTaskCreateParams struct {
	ContactID      *string    `json:"contact_id,omitempty"`
	DealID         *string    `json:"deal_id,omitempty"`
	AssignedTo     *string    `json:"assigned_to,omitempty"`
	AssignedTeamID *string    `json:"assigned_team_id,omitempty"`
	Title          string     `json:"title"`
	Description    *string    `json:"description,omitempty"`
	DueDate        *time.Time `json:"due_date,omitempty"`
	Priority       string     `json:"priority,omitempty"`
	Type           string     `json:"type,omitempty"`
}

CRMTaskCreateParams creates a task. Title is required.

type CRMTaskListParams added in v0.3.0

type CRMTaskListParams struct {
	ListOptions
	// ContactID and DealID narrow to the tasks hanging off one record.
	ContactID string
	DealID    string
	// AssignedTo is an assignee user id.
	AssignedTo string
	// Status is one of the TaskStatus* constants.
	Status string
}

CRMTaskListParams filters and paginates the plain task list. Every filter is optional and matches a single value; use TaskSearchParams with CRMService.SearchTasks when you need several values per facet, due-date bounds or an exact total.

type CRMTaskType

type CRMTaskType struct {
	ID             string    `json:"id"`
	OrganizationID string    `json:"organization_id"`
	Name           string    `json:"name"`
	Color          string    `json:"color"`
	Position       int       `json:"position"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

CRMTaskType is a user-defined kind of work, for example Call or Meeting. Tasks reference their type by name, so deleting a type never orphans the tasks that used it; they keep the label and fall back to a neutral color.

type CRMTaskUpdateParams

type CRMTaskUpdateParams struct {
	AssignedTo     *string    `json:"assigned_to,omitempty"`
	AssignedTeamID *string    `json:"assigned_team_id,omitempty"`
	Title          *string    `json:"title,omitempty"`
	Description    *string    `json:"description,omitempty"`
	DueDate        *time.Time `json:"due_date,omitempty"`
	Priority       *string    `json:"priority,omitempty"`
	Type           *string    `json:"type,omitempty"`
	Status         *string    `json:"status,omitempty"`
}

CRMTaskUpdateParams updates a task. Nil fields are left unchanged.

type Campaign

type Campaign struct {
	ID             string  `json:"id"`
	UserID         string  `json:"user_id"`
	OrganizationID *string `json:"organization_id,omitempty"`

	Name        string `json:"name"`
	Description string `json:"description"`
	// Status is one of the CampaignStatus* constants.
	Status string `json:"status"`
	// Kind is [CampaignKindSequence] or [CampaignKindOneTime].
	Kind string `json:"kind"`

	// StopOnReply halts a contact's sequence as soon as they reply.
	StopOnReply bool `json:"stop_on_reply"`
	// OpenTracking enables open tracking via a tracking pixel.
	OpenTracking bool `json:"open_tracking"`
	// LinkTracking enables click tracking by rewriting links.
	LinkTracking bool `json:"link_tracking"`
	// TextOnly sends the plain-text body only, with no HTML part.
	TextOnly bool `json:"text_only"`
	// DailyLimit caps sends per mailbox per day for this campaign; each
	// mailbox sends the smaller of this and its own cap.
	DailyLimit int `json:"daily_limit"`
	// UnsubscribeHeader adds RFC 8058 List-Unsubscribe headers.
	UnsubscribeHeader bool `json:"unsubscribe_header"`
	// RiskyEmails allows sending to addresses verification flagged as risky.
	RiskyEmails bool `json:"risky_emails"`
	// UnsubscribeMode picks the in-body opt-out appended after the signature:
	// [UnsubscribeModeInherit] follows the organization setting in
	// [OutreachSettings.Unsubscribe]; the other UnsubscribeMode* constants
	// override it for this campaign.
	UnsubscribeMode string `json:"unsubscribe_mode"`

	// CC and BCC are copied on every send.
	CC  []string `json:"cc"`
	BCC []string `json:"bcc"`

	// StartDate and EndDate bound the active sending window. Both are nullable:
	// a nil StartDate means "start now" and a nil EndDate means open-ended.
	StartDate *time.Time `json:"start_date"`
	EndDate   *time.Time `json:"end_date"`
	// Timezone is the IANA timezone the schedule is interpreted in.
	Timezone string `json:"timezone"`
	// Days is a legacy weekday bitmask (bit 0 is Monday), superseded by
	// ScheduleWindows.
	Days uint8 `json:"days"`
	// StartTime and EndTime are legacy "HH:MM" bounds, superseded by
	// ScheduleWindows.
	StartTime string `json:"start_time"`
	EndTime   string `json:"end_time"`
	// ScheduleWindows is the authoritative per-day schedule when non-empty.
	ScheduleWindows ScheduleWindows `json:"schedule_windows"`

	// EmailTags holds the mailbox tag ids the sending pool is drawn from under
	// [SenderStrategyTags]. Folders holds the campaign's folder ids.
	EmailTags []string `json:"email_tags"`
	Folders   []string `json:"folders"`

	// ContactOrderBy, ContactOrderDir and ContactOrderField control the order
	// contacts are enrolled in.
	ContactOrderBy    string  `json:"contact_order_by"`
	ContactOrderDir   string  `json:"contact_order_dir"`
	ContactOrderField *string `json:"contact_order_field,omitempty"`

	// SenderStrategy is [SenderStrategyTags] or [SenderStrategyExplicit];
	// RotationMode picks how volume spreads across the chosen mailboxes.
	SenderStrategy string `json:"sender_strategy"`
	RotationMode   string `json:"rotation_mode"`
	// Senders is the explicit pool. It is loaded on demand rather than on every
	// read; use [CampaignService.Senders] to fetch it.
	Senders []CampaignSender `json:"senders,omitempty"`

	// RampEnabled gradually increases daily volume for the campaign. The ramp
	// only ever lowers volume: it is applied as a minimum against each
	// mailbox's own cap. RampLevel is server-managed and survives pause/resume.
	RampEnabled   bool       `json:"ramp_enabled"`
	RampStart     int        `json:"ramp_start"`
	RampIncrement int        `json:"ramp_increment"`
	RampCeiling   int        `json:"ramp_ceiling"`
	RampLevel     int        `json:"ramp_level"`
	RampLevelDate *time.Time `json:"ramp_level_date,omitempty"`

	// ESPMatchMode is [ESPMatchOff], [ESPMatchPrefer] or [ESPMatchStrict].
	ESPMatchMode string `json:"esp_match_mode"`

	// MaxNewLeadsPerDay throttles newly enrolled contacts; 0 means unlimited.
	MaxNewLeadsPerDay  int  `json:"max_new_leads_per_day"`
	PrioritizeNewLeads bool `json:"prioritize_new_leads"`

	// Continuous keeps the campaign active when it runs out of leads: instead
	// of completing it waits, with IdleSince set, and sends the sequence to
	// each lead as they arrive (from a linked segment, a form, the API or an
	// automation). Anything that feeds it leads turns it on: linking a segment
	// or a form, an automation that enrolls into it, or starting a campaign
	// whose every lead has finished. A continuous campaign can be started with
	// no leads at all; only its end date finishes it.
	Continuous bool `json:"continuous"`
	// IdleSince is set while a continuous campaign is waiting for leads and
	// cleared as soon as it has something to send again.
	IdleSince *time.Time `json:"idle_since,omitempty"`

	// Auto-pause guardrails. Rates are percentages (0-100) evaluated over a
	// rolling GuardrailWindowDays window every 15 minutes, and the campaign is
	// moved to [CampaignStatusPausedGuardrail] the moment a band is breached.
	// A rate of 0 disables that rule; GuardrailMinSample is the number of sends
	// in the window below which no rule fires. GuardrailWindowDays 0 measures
	// the campaign's whole history.
	//
	// Bounce and complaint rates are ceilings (pause at or above); the reply
	// rate is a floor (pause below). Off by default.
	GuardrailEnabled          bool    `json:"guardrail_enabled"`
	GuardrailBounceRateMax    float64 `json:"guardrail_bounce_rate_max"`
	GuardrailComplaintRateMax float64 `json:"guardrail_complaint_rate_max"`
	GuardrailReplyRateMin     float64 `json:"guardrail_reply_rate_min"`
	GuardrailMinSample        int     `json:"guardrail_min_sample"`
	GuardrailWindowDays       int     `json:"guardrail_window_days"`
	// GuardrailTrippedAt and GuardrailReason are server-owned: set when a
	// guardrail pauses the campaign and cleared when it is started again.
	GuardrailTrippedAt *time.Time `json:"guardrail_tripped_at,omitempty"`
	GuardrailReason    string     `json:"guardrail_reason,omitempty"`

	// TrackingDomain overrides the mailbox tracking domain for this campaign.
	// It is honored only once verified.
	TrackingDomain           string     `json:"tracking_domain"`
	TrackingDomainVerified   bool       `json:"tracking_domain_verified"`
	TrackingDomainVerifiedAt *time.Time `json:"tracking_domain_verified_at,omitempty"`

	// UTMTracking tags every http(s) link in the body with utm_* parameters at
	// send time; a link that already carries one keeps its own value. Empty
	// UTMSource, UTMMedium and UTMCampaign mean the defaults ("warmbly",
	// "email" and the campaign name as a slug); utm_content is always the
	// link's own text. Off for campaigns created through the API unless sent.
	UTMTracking bool   `json:"utm_tracking"`
	UTMSource   string `json:"utm_source"`
	UTMMedium   string `json:"utm_medium"`
	UTMCampaign string `json:"utm_campaign"`

	LastStatusChangeAt *time.Time `json:"last_status_change_at,omitempty"`

	UpdatedAt time.Time `json:"updated_at"`
	CreatedAt time.Time `json:"created_at"`
}

Campaign is an outreach campaign as returned by the API.

type CampaignAdvancedSettings

type CampaignAdvancedSettings struct {
	CampaignID string           `json:"campaign_id"`
	Overrides  OutreachSettings `json:"overrides"`
	UpdatedAt  time.Time        `json:"updated_at"`
}

CampaignAdvancedSettings is a campaign's override of the organization-wide outreach policy.

type CampaignAnalytics

type CampaignAnalytics struct {
	CampaignID string                  `json:"campaign_id"`
	Name       string                  `json:"name"`
	Status     string                  `json:"status"`
	DateRange  DateRange               `json:"date_range"`
	Summary    CampaignAnalyticsTotals `json:"summary"`
	Steps      []StepAnalytics         `json:"steps"`
	DailyStats []DailyStat             `json:"daily_stats,omitempty"`
	// Engagement is the country, mail-client and device breakdown of human
	// opens and clicks. It is best-effort: nil when the breakdown could not be
	// computed, in which case Summary still stands on its own.
	Engagement *CampaignEngagement `json:"engagement,omitempty"`
}

CampaignAnalytics is one campaign's engagement, broken down by step and, for human opens and clicks, by where and on what they happened.

type CampaignAnalyticsTotals

type CampaignAnalyticsTotals struct {
	TotalContacts int64 `json:"total_contacts"`
	EmailsSent    int64 `json:"emails_sent"`
	// EmailsPending are queued sends not yet dispatched.
	EmailsPending int64 `json:"emails_pending"`
	UniqueOpens   int64 `json:"unique_opens"`
	// MachineOpens is the subset of UniqueOpens from automated fetchers
	// (mail-privacy prefetch, UA-less clients). Human opens are
	// UniqueOpens - MachineOpens.
	MachineOpens int64 `json:"machine_opens"`
	UniqueClicks int64 `json:"unique_clicks"`
	// MachineClicks counts steps whose only clicks came from automated
	// fetchers. They are not part of UniqueClicks, which only ever counts a
	// person's click.
	MachineClicks int64   `json:"machine_clicks"`
	Replies       int64   `json:"replies"`
	Bounces       int64   `json:"bounces"`
	Unsubscribes  int64   `json:"unsubscribes"`
	OpenRate      float64 `json:"open_rate"`
	ClickRate     float64 `json:"click_rate"`
	ReplyRate     float64 `json:"reply_rate"`
	BounceRate    float64 `json:"bounce_rate"`
}

CampaignAnalyticsTotals are a campaign's headline counters.

type CampaignAttachment

type CampaignAttachment struct {
	ID         string `json:"id"`
	CampaignID string `json:"campaign_id"`
	// StepID is the step the file is sent with; nil means it rides every step
	// of the campaign. The field is always present.
	StepID   *string `json:"step_id"`
	Filename string  `json:"filename"`
	Size     int64   `json:"size"`
	MimeType string  `json:"mime_type"`
	// URL is a presigned download link, valid for about 15 minutes after the
	// response; fetch the attachment again for a fresh one.
	URL       string    `json:"url"`
	CreatedAt time.Time `json:"created_at"`
}

CampaignAttachment is a file attached to a campaign, optionally scoped to a single step.

type CampaignComparison

type CampaignComparison struct {
	Campaigns []CampaignSummary `json:"campaigns"`
	Period    DateRange         `json:"period"`
}

CampaignComparison compares several campaigns over one window.

type CampaignCreateParams

type CampaignCreateParams struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	// Kind is [CampaignKindSequence] (the default) or [CampaignKindOneTime].
	// It is fixed at creation. A one-time email accepts at most one entry in
	// Steps.
	Kind *string `json:"kind,omitempty"`

	StopOnReply       *bool `json:"stop_on_reply,omitempty"`
	OpenTracking      *bool `json:"open_tracking,omitempty"`
	LinkTracking      *bool `json:"link_tracking,omitempty"`
	TextOnly          *bool `json:"text_only,omitempty"`
	DailyLimit        *int  `json:"daily_limit,omitempty"`
	UnsubscribeHeader *bool `json:"unsubscribe_header,omitempty"`
	RiskyEmails       *bool `json:"risky_emails,omitempty"`
	// UnsubscribeMode is one of the UnsubscribeMode* constants; the default
	// is [UnsubscribeModeInherit].
	UnsubscribeMode *string `json:"unsubscribe_mode,omitempty"`

	CC  []string `json:"cc,omitempty"`
	BCC []string `json:"bcc,omitempty"`

	StartDate       *time.Time       `json:"start_date,omitempty"`
	EndDate         *time.Time       `json:"end_date,omitempty"`
	Timezone        *string          `json:"timezone,omitempty"`
	Days            *uint8           `json:"days,omitempty"`
	StartTime       *string          `json:"start_time,omitempty"`
	EndTime         *string          `json:"end_time,omitempty"`
	ScheduleWindows *ScheduleWindows `json:"schedule_windows,omitempty"`

	// EmailTagIDs and FolderIDs reference tags and folders that already exist.
	EmailTagIDs []string `json:"email_tag_ids,omitempty"`
	FolderIDs   []string `json:"folder_ids,omitempty"`

	SenderStrategy *string               `json:"sender_strategy,omitempty"`
	RotationMode   *string               `json:"rotation_mode,omitempty"`
	Senders        []CampaignSenderInput `json:"senders,omitempty"`

	RampEnabled   *bool `json:"ramp_enabled,omitempty"`
	RampStart     *int  `json:"ramp_start,omitempty"`
	RampIncrement *int  `json:"ramp_increment,omitempty"`
	RampCeiling   *int  `json:"ramp_ceiling,omitempty"`

	ESPMatchMode       *string `json:"esp_match_mode,omitempty"`
	MaxNewLeadsPerDay  *int    `json:"max_new_leads_per_day,omitempty"`
	PrioritizeNewLeads *bool   `json:"prioritize_new_leads,omitempty"`
	// Continuous keeps the campaign active and waiting when it runs out of
	// leads; see [Campaign.Continuous].
	Continuous     *bool   `json:"continuous,omitempty"`
	TrackingDomain *string `json:"tracking_domain,omitempty"`

	// UTMTracking is off unless sent; empty UTMSource, UTMMedium and
	// UTMCampaign keep the defaults. See [Campaign.UTMTracking].
	UTMTracking *bool   `json:"utm_tracking,omitempty"`
	UTMSource   *string `json:"utm_source,omitempty"`
	UTMMedium   *string `json:"utm_medium,omitempty"`
	UTMCampaign *string `json:"utm_campaign,omitempty"`

	// Steps seeds the sequence in order, each connected to the previous one
	// with its wait set. Steps can equally be added afterwards with
	// [CampaignService.CreateStep].
	Steps []StepInput `json:"steps,omitempty"`
	// Variants seeds A/B variants for the first step.
	Variants []ABVariantCreateParams `json:"variants,omitempty"`
	// AdvancedOverrides seeds this campaign's outreach-policy overrides.
	AdvancedOverrides *OutreachSettings `json:"advanced_overrides,omitempty"`
}

CampaignCreateParams creates a campaign. Only Name is required; every other field falls back to a server default. The wizard sends everything at once, while a simple create can send just a name and description.

Segments are linked after creation with CampaignService.SetSegments, and auto-pause guardrails are configured with CampaignService.Update.

type CampaignDuplicateParams added in v0.3.0

type CampaignDuplicateParams struct {
	// Name is the copy's name, 3 to 50 characters. Empty defaults to the source
	// name with " (copy)" appended.
	Name string `json:"name,omitempty"`
}

CampaignDuplicateParams is the optional body of CampaignService.Duplicate.

type CampaignEngagement added in v0.3.0

type CampaignEngagement struct {
	// Countries is keyed by ISO 3166-1 alpha-2 country code.
	Countries []EngagementBucket `json:"countries"`
	// Clients is keyed by mail client or browser name.
	Clients []EngagementBucket `json:"clients"`
	// Devices is keyed by device type, for example "desktop" or "mobile".
	Devices []EngagementBucket `json:"devices"`
}

CampaignEngagement is the "where from, on what" view of a campaign's human opens and clicks. Each list is ordered by activity and capped at the busiest buckets; an empty key means unknown.

type CampaignEstimateParams added in v0.3.0

type CampaignEstimateParams struct {
	// SegmentIDs make up the audience (at most 20). A contact in several of
	// them is counted once.
	SegmentIDs []string `json:"segment_ids"`
	// EmailTagIDs resolve the mailbox pool. Empty means every active mailbox
	// in the organization.
	EmailTagIDs []string `json:"email_tag_ids,omitempty"`
	// DailyLimit is the per-mailbox campaign cap to apply (default 50). Each
	// mailbox counts the smaller of this and its own cap.
	DailyLimit *int `json:"daily_limit,omitempty"`
	// Days is the weekday bitmask of sending days, bit 0 being Monday.
	// Defaults to weekdays.
	Days *uint8 `json:"days,omitempty"`
	// Timezone is the IANA timezone the days are counted in. Defaults to UTC.
	Timezone *string `json:"timezone,omitempty"`
	// StartDate is when sending begins. Omit for now.
	StartDate *time.Time `json:"start_date,omitempty"`
}

CampaignEstimateParams projects an audience against a sender pool before a campaign exists. Only SegmentIDs is required. Nothing is written.

type CampaignEstimateResult added in v0.3.0

type CampaignEstimateResult struct {
	Recipients int `json:"recipients"`
	Mailboxes  int `json:"mailboxes"`
	// DailyCapacity is the pool's per-day ceiling under the campaign limit;
	// RemainingToday subtracts what the mailboxes already sent today.
	DailyCapacity  int `json:"daily_capacity"`
	RemainingToday int `json:"remaining_today"`
	// SendingDays is how many sending days the audience needs and
	// EstimatedFinishAt the calendar day the last send lands on. Both are nil
	// when the audience is empty, the pool has no capacity, or the send would
	// take longer than two years.
	SendingDays       *int       `json:"sending_days"`
	EstimatedFinishAt *time.Time `json:"estimated_finish_at"`
}

CampaignEstimateResult is the projection from CampaignService.Estimate. It applies the scheduler's cap rule but none of its pacing, so it is the earliest the last send can land, not a promise.

type CampaignFolderCount

type CampaignFolderCount struct {
	FolderID string `json:"folder_id"`
	Total    int64  `json:"total"`
}

CampaignFolderCount is one folder's campaign total.

type CampaignFormStats added in v0.3.0

type CampaignFormStats struct {
	FormID   string `json:"form_id"`
	FormName string `json:"form_name"`
	// PublicID is the form's public slug.
	PublicID string `json:"public_id"`
	// Status is the form's publication status.
	Status      string `json:"status"`
	LinksSent   int64  `json:"links_sent"`
	Viewers     int64  `json:"viewers"`
	Starters    int64  `json:"starters"`
	Submissions int64  `json:"submissions"`
	// ShareURL is the form's public URL, when it is published.
	ShareURL string `json:"share_url,omitempty"`
}

CampaignFormStats is one form the campaign's emails link to, with what the campaign's recipients did with it. LinksSent counts personalized links minted for recipients; Viewers, Starters and Submissions are distinct recipients who opened, began and completed it.

type CampaignLeadCounts

type CampaignLeadCounts struct {
	Total int `json:"total"`
	// Queued is the pending bucket; Processing is the active bucket.
	Queued       int `json:"queued"`
	Processing   int `json:"processing"`
	Completed    int `json:"completed"`
	Replied      int `json:"replied"`
	Bounced      int `json:"bounced"`
	Failed       int `json:"failed"`
	Unsubscribed int `json:"unsubscribed"`
	// Undeliverable leads were refused by address verification.
	Undeliverable int `json:"undeliverable"`

	// Engagement totals matching the LeadEngagement* filters: leads sent at
	// least one step, and of those the ones with a human open, a click, or a
	// reply on any step (whatever their derived status).
	Contacted  int `json:"contacted"`
	Opened     int `json:"opened"`
	Clicked    int `json:"clicked"`
	RepliedAny int `json:"replied_any"`
}

CampaignLeadCounts are per-status lead totals within a single campaign, returned on the first page of a search that filters by exactly one campaign. They ignore the search's own lead-status and engagement filters, so every bucket shows its real total.

type CampaignListParams

type CampaignListParams struct {
	ListOptions
	// Query is a free-text filter on campaign name.
	Query string
	// Folder restricts the list to a single folder id.
	Folder string
	// Status is a bucket filter: [CampaignStatusDraft], [CampaignStatusActive],
	// [CampaignStatusPaused] (matches every paused_* variant) or
	// [CampaignStatusCompleted]. Any other value is a 400.
	Status string
	// Kind is [CampaignKindSequence] or [CampaignKindOneTime]. Any other value
	// is a 400.
	Kind string
}

CampaignListParams filters and paginates a list of campaigns.

type CampaignLogEntry

type CampaignLogEntry struct {
	ID         string `json:"id"`
	CampaignID string `json:"campaign_id"`
	// EventType names what happened, for example "started", "created" (also
	// written for a duplicate, with source_campaign_id in Metadata) or "idle"
	// when a continuous campaign runs out of leads and waits.
	EventType string         `json:"event_type"`
	Message   string         `json:"message"`
	Metadata  map[string]any `json:"metadata,omitempty"`
	CreatedAt time.Time      `json:"created_at"`
}

CampaignLogEntry is a single entry from a campaign's activity log.

type CampaignSegmentLink struct {
	SegmentID    string    `json:"segment_id"`
	Name         string    `json:"name"`
	Color        string    `json:"color"`
	Description  string    `json:"description"`
	ContactCount int       `json:"contact_count"`
	LeadCount    int       `json:"lead_count"`
	HeldOutCount int       `json:"held_out_count"`
	LinkedAt     time.Time `json:"linked_at"`
}

CampaignSegmentLink is one segment linked to a campaign as a live audience source. The counts are evaluated when asked: ContactCount is how many contacts the segment matches now, LeadCount how many of them are leads of this campaign, and HeldOutCount how many are not leads because they were removed from the campaign by hand and are never re-added automatically.

type CampaignSegmentsResult added in v0.3.0

type CampaignSegmentsResult struct {
	Segments []CampaignSegmentLink `json:"data"`
	Added    int                   `json:"added"`
}

CampaignSegmentsResult is the outcome of CampaignService.SetSegments: the resulting links plus how many leads the call enrolled. Added is 0 when every member was already a lead, the segments match no contacts yet, or the only members are held out; the per-link counts tell these apart.

type CampaignSender

type CampaignSender struct {
	EmailAccountID string     `json:"email_account_id"`
	Weight         int        `json:"weight"`
	LastSentAt     *time.Time `json:"last_sent_at,omitempty"`
	Enabled        bool       `json:"enabled"`
}

CampaignSender is one mailbox in an explicit-strategy campaign's sender pool.

type CampaignSenderInput

type CampaignSenderInput struct {
	EmailAccountID string `json:"email_account_id"`
	Weight         *int   `json:"weight,omitempty"`
	Enabled        *bool  `json:"enabled,omitempty"`
}

CampaignSenderInput adds or updates one mailbox in the sender pool.

type CampaignService

type CampaignService service

CampaignService manages outreach campaigns: their schedule and sending policy, their sequence steps, their sender pool, linked segments, A/B variants, attachments and the preflight checks run before they go live.

A campaign is an ordered sequence of steps (emails, waits and actions) delivered to enrolled contacts from one or more connected mailboxes. Steps are addressed as a sub-resource under their campaign.

func (*CampaignService) ABAnalysis

func (s *CampaignService) ABAnalysis(ctx context.Context, id string, opts ...RequestOption) (*ABAnalysis, *Response, error)

ABAnalysis compares the campaign's A/B arms and names a winner once the result is significant.

func (*CampaignService) AdvancedSettings

func (s *CampaignService) AdvancedSettings(ctx context.Context, id string, opts ...RequestOption) (*CampaignAdvancedSettings, *Response, error)

AdvancedSettings returns the campaign's overrides of the organization outreach policy.

func (*CampaignService) Create

Create creates a new campaign. It starts as a draft and sends nothing until started.

func (*CampaignService) CreateABVariant

func (s *CampaignService) CreateABVariant(ctx context.Context, id string, params *ABVariantCreateParams, opts ...RequestOption) (*ABVariant, *Response, error)

CreateABVariant adds an A/B variant to the campaign.

func (*CampaignService) CreateStep

func (s *CampaignService) CreateStep(ctx context.Context, id string, opts ...RequestOption) (*Step, *Response, error)

CreateStep appends a blank step to the campaign's sequence. Fill it in with CampaignService.UpdateStep. New steps are not connected to anything: wire them in through the previous step's Conditions. A one-time campaign refuses a second email step.

func (*CampaignService) Delete

func (s *CampaignService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete permanently removes a campaign with its steps, lead progress and activity. A running campaign can be deleted directly: its pending sends are canceled with it. Contacts and emails already sent stay.

func (*CampaignService) DeleteABVariant

func (s *CampaignService) DeleteABVariant(ctx context.Context, id, variantID string, opts ...RequestOption) (*Response, error)

DeleteABVariant removes an A/B variant.

func (*CampaignService) DeleteAttachment

func (s *CampaignService) DeleteAttachment(ctx context.Context, id, attachmentID string, opts ...RequestOption) (*Response, error)

DeleteAttachment removes an attachment from the campaign and from storage.

func (*CampaignService) DeleteStep

func (s *CampaignService) DeleteStep(ctx context.Context, id, stepID string, opts ...RequestOption) (*Response, error)

DeleteStep removes a step from the campaign's sequence, along with the attachments scoped to it.

func (*CampaignService) Duplicate added in v0.3.0

func (s *CampaignService) Duplicate(ctx context.Context, id string, params *CampaignDuplicateParams, opts ...RequestOption) (*Campaign, *Response, error)

Duplicate creates a draft copy of a campaign's configuration: every setting, the steps with their branch graph and canvas positions, tags, folders, the explicit sender list, A/B variants, advanced settings and attachments. Leads, progress, statistics, the activity log, the ramp level, a guardrail trip and any start or end date already in the past are not copied. The copy is owned by the caller, counts against the daily new-campaign throttle, and answers 201. params may be nil.

func (*CampaignService) Estimate added in v0.3.0

Estimate projects an audience of segments against a sender pool before a campaign exists: how many contacts it resolves to, the pool's daily capacity and the day the last send is expected to land. It writes nothing, so it needs no idempotency key.

func (*CampaignService) Forms added in v0.3.0

Forms returns the forms the campaign's emails link to, with what the campaign's recipients did with each.

func (*CampaignService) Get

func (s *CampaignService) Get(ctx context.Context, id string, opts ...RequestOption) (*Campaign, *Response, error)

Get retrieves a single campaign by ID.

func (*CampaignService) List

func (s *CampaignService) List(ctx context.Context, params *CampaignListParams, opts ...RequestOption) (*Page[Campaign], error)

List returns a page of campaigns. The page total counts campaigns matching the Query, Folder and Status filters.

func (*CampaignService) ListABVariants

func (s *CampaignService) ListABVariants(ctx context.Context, id string, opts ...RequestOption) ([]ABVariant, *Response, error)

ListABVariants returns the campaign's A/B variants.

func (*CampaignService) ListAttachments

func (s *CampaignService) ListAttachments(ctx context.Context, id string, opts ...RequestOption) ([]CampaignAttachment, *Response, error)

ListAttachments returns the campaign's attachments, campaign-wide and per-step alike.

func (*CampaignService) ListSegments added in v0.3.0

func (s *CampaignService) ListSegments(ctx context.Context, id string, opts ...RequestOption) ([]CampaignSegmentLink, *Response, error)

ListSegments returns the segments linked to the campaign as live audience sources, with their current member and lead counts.

func (*CampaignService) ListSteps

func (s *CampaignService) ListSteps(ctx context.Context, id string, opts ...RequestOption) ([]Step, *Response, error)

ListSteps returns the campaign's sequence steps in order.

func (*CampaignService) Logs

func (s *CampaignService) Logs(ctx context.Context, id string, params *ListOptions, opts ...RequestOption) (*Page[CampaignLogEntry], error)

Logs returns a page of a campaign's activity log, newest first. Limit is capped at 100.

func (*CampaignService) Overview

Overview returns status-bucket and per-folder campaign counts for the organization.

func (*CampaignService) Preflight

func (s *CampaignService) Preflight(ctx context.Context, id string, opts ...RequestOption) (*PreflightResult, *Response, error)

Preflight runs the pre-launch checks for a campaign without starting it.

func (*CampaignService) PreviewTemplate

PreviewTemplate renders campaign copy the way the send path would. It is not scoped to a campaign in the path (name one in the params to include its footer and attachments) and sends nothing.

func (*CampaignService) ReplaceSenders

func (s *CampaignService) ReplaceSenders(ctx context.Context, id string, senders []CampaignSenderInput, opts ...RequestOption) ([]CampaignSender, *Response, error)

ReplaceSenders replaces the campaign's explicit sender pool wholesale.

func (*CampaignService) SendTestEmail

func (s *CampaignService) SendTestEmail(ctx context.Context, id string, params *TestEmailParams, opts ...RequestOption) (*TestEmailResult, *Response, error)

SendTestEmail sends a rendered preview of a step to a single address.

func (*CampaignService) Senders

func (s *CampaignService) Senders(ctx context.Context, id string, opts ...RequestOption) ([]CampaignSender, *Response, error)

Senders returns the campaign's explicit sender pool.

func (*CampaignService) SetSegments added in v0.3.0

func (s *CampaignService) SetSegments(ctx context.Context, id string, segmentIDs []string, opts ...RequestOption) (*CampaignSegmentsResult, *Response, error)

SetSegments atomically replaces the campaign's linked segments (up to 20) and turns Campaign.Continuous on. Every current member of a newly linked segment is enrolled as a lead immediately, and contacts who enter a linked segment later are enrolled automatically within about two minutes. Enrolment is additive: a contact who leaves a segment keeps their lead row, and unlinking a segment stops future enrolment without touching existing leads. An active campaign wakes to send to the new leads; a completed one restarts through the launch checks when a linked segment grows.

An empty segmentIDs detaches every segment (the SDK sends an explicit empty array, which the API requires). The links and the enrolment are written in one transaction, so retries are safe.

func (*CampaignService) Start

Start begins (or resumes) sending for a campaign. It works from draft, any paused status or completed; a campaign closed by a passed end date resumes once that date is extended or cleared. Status changes are rate-limited to one per minute per campaign.

A campaign with nothing left to send does not finish again. The start turns CampaignUpdateParams.Continuous on if it was off, leaves the campaign CampaignStatusActive with Campaign.IdleSince set, and answers CampaignStatusChange.WaitingForLeads; the switch is written to the campaign's activity log. Adding a lead by any path then wakes it.

The start can be refused with ErrCodeListBounceRisk, ErrCodeLeadsUndeliverable or ErrCodeNoLeads in Error.Code; see CampaignService.StartWithOptions to launch past the bounce-risk gate.

func (*CampaignService) StartWithOptions added in v0.3.0

func (s *CampaignService) StartWithOptions(ctx context.Context, id string, params *CampaignStartParams, opts ...RequestOption) (*CampaignStatusChange, *Response, error)

StartWithOptions is CampaignService.Start with a body; params may be nil, in which case no body is sent.

func (*CampaignService) Stop

Stop pauses sending for a campaign. Leads keep their place and resume from it on the next start.

func (*CampaignService) Update

func (s *CampaignService) Update(ctx context.Context, id string, params *CampaignUpdateParams, opts ...RequestOption) (*Campaign, *Response, error)

Update modifies a campaign's settings.

func (*CampaignService) UpdateABVariant

func (s *CampaignService) UpdateABVariant(ctx context.Context, id, variantID string, params *ABVariantUpdateParams, opts ...RequestOption) (*ABVariant, *Response, error)

UpdateABVariant modifies an A/B variant.

func (*CampaignService) UpdateAdvancedSettings

func (s *CampaignService) UpdateAdvancedSettings(ctx context.Context, id string, overrides *OutreachSettings, opts ...RequestOption) (*Response, error)

UpdateAdvancedSettings replaces the campaign's outreach-policy overrides. The API answers 204 with no body; read them back with CampaignService.AdvancedSettings.

func (*CampaignService) UpdateStep

func (s *CampaignService) UpdateStep(ctx context.Context, id, stepID string, params *StepUpdateParams, opts ...RequestOption) (*Step, *Response, error)

UpdateStep modifies a step's content, delay, routing or kind.

func (*CampaignService) UpdateStepLayout

func (s *CampaignService) UpdateStepLayout(ctx context.Context, id string, positions []StepPosition, opts ...RequestOption) (*Response, error)

UpdateStepLayout persists the canvas coordinates of a campaign's steps. It is cosmetic: it does not audit, does not bump the campaign's updated_at, and is last-write-wins, so retries are safe. At most 1000 positions per call.

func (*CampaignService) UploadAttachment

func (s *CampaignService) UploadAttachment(ctx context.Context, id string, file *FileUpload, stepID string, opts ...RequestOption) (*CampaignAttachment, *Response, error)

UploadAttachment attaches a file to the campaign. With stepID it is sent only with that step (which must belong to the campaign); without, it rides every step. Files are capped at 15 MB, executable and script types are refused, and the upload counts against the organization's storage quota (a 4xx with code "storage_limit_reached" when it would pass it). Answers 201.

func (*CampaignService) VerifyTrackingDomain

func (s *CampaignService) VerifyTrackingDomain(ctx context.Context, id string, opts ...RequestOption) (*TrackingDomainStatus, *Response, error)

VerifyTrackingDomain re-resolves the campaign's tracking-domain override.

type CampaignStartParams added in v0.3.0

type CampaignStartParams struct {
	// AcknowledgeListRisk launches past the bounce-risk gate
	// ([ErrCodeListBounceRisk]), for a list verified elsewhere.
	AcknowledgeListRisk bool `json:"acknowledge_list_risk"`
}

CampaignStartParams qualifies a CampaignService.StartWithOptions request.

type CampaignStatusChange added in v0.3.0

type CampaignStatusChange struct {
	Status string `json:"status"`
	// WaitingForLeads is true when a start found nothing left to send: the
	// campaign is [CampaignStatusActive] with [Campaign.IdleSince] set, waiting
	// for leads rather than finishing. Starting a campaign whose every lead has
	// completed the sequence turns [CampaignUpdateParams.Continuous] on to get
	// there, so a second start never answers no_remaining_leads.
	//
	// It is always false on a stop.
	WaitingForLeads bool `json:"waiting_for_leads"`
}

CampaignStatusChange confirms a start or stop. Status is "started" or "stopped"; fetch the campaign for its resulting state.

type CampaignSummary

type CampaignSummary struct {
	CampaignID string  `json:"campaign_id"`
	Name       string  `json:"name"`
	Status     string  `json:"status"`
	EmailsSent int64   `json:"emails_sent"`
	OpenRate   float64 `json:"open_rate"`
	ClickRate  float64 `json:"click_rate"`
	ReplyRate  float64 `json:"reply_rate"`
	BounceRate float64 `json:"bounce_rate,omitempty"`
}

CampaignSummary is one campaign's headline engagement.

type CampaignUpdateParams

type CampaignUpdateParams struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	Status      *string `json:"status,omitempty"`

	StopOnReply       *bool `json:"stop_on_reply,omitempty"`
	OpenTracking      *bool `json:"open_tracking,omitempty"`
	LinkTracking      *bool `json:"link_tracking,omitempty"`
	TextOnly          *bool `json:"text_only,omitempty"`
	DailyLimit        *int  `json:"daily_limit,omitempty"`
	UnsubscribeHeader *bool `json:"unsubscribe_header,omitempty"`
	RiskyEmails       *bool `json:"risky_emails,omitempty"`
	// UnsubscribeMode is one of the UnsubscribeMode* constants.
	UnsubscribeMode *string `json:"unsubscribe_mode,omitempty"`

	CC  []string `json:"cc,omitempty"`
	BCC []string `json:"bcc,omitempty"`

	// StartDate and EndDate set the sending window. To clear a stored date
	// (start now / run open-ended) leave the pointer nil and set
	// ClearStartDate or ClearEndDate, which sends an explicit null.
	StartDate *time.Time `json:"start_date,omitempty"`
	EndDate   *time.Time `json:"end_date,omitempty"`
	// ClearStartDate and ClearEndDate null out the stored dates. They take
	// precedence over StartDate and EndDate.
	ClearStartDate bool `json:"-"`
	ClearEndDate   bool `json:"-"`

	Timezone        *string          `json:"timezone,omitempty"`
	Days            *uint8           `json:"days,omitempty"`
	StartTime       *string          `json:"start_time,omitempty"`
	EndTime         *string          `json:"end_time,omitempty"`
	ScheduleWindows *ScheduleWindows `json:"schedule_windows,omitempty"`

	EmailTags []string `json:"email_tags,omitempty"`
	Folders   []string `json:"folders,omitempty"`

	ContactOrderBy    *string `json:"contact_order_by,omitempty"`
	ContactOrderDir   *string `json:"contact_order_dir,omitempty"`
	ContactOrderField *string `json:"contact_order_field,omitempty"`

	SenderStrategy *string `json:"sender_strategy,omitempty"`
	RotationMode   *string `json:"rotation_mode,omitempty"`

	RampEnabled   *bool `json:"ramp_enabled,omitempty"`
	RampStart     *int  `json:"ramp_start,omitempty"`
	RampIncrement *int  `json:"ramp_increment,omitempty"`
	RampCeiling   *int  `json:"ramp_ceiling,omitempty"`

	ESPMatchMode       *string `json:"esp_match_mode,omitempty"`
	MaxNewLeadsPerDay  *int    `json:"max_new_leads_per_day,omitempty"`
	PrioritizeNewLeads *bool   `json:"prioritize_new_leads,omitempty"`
	// Continuous keeps the campaign active and waiting when it runs out of
	// leads; see [Campaign.Continuous]. Turning it off has the campaign finish
	// once every lead is done.
	Continuous     *bool   `json:"continuous,omitempty"`
	TrackingDomain *string `json:"tracking_domain,omitempty"`

	// UTM tagging; see [Campaign.UTMTracking].
	UTMTracking *bool   `json:"utm_tracking,omitempty"`
	UTMSource   *string `json:"utm_source,omitempty"`
	UTMMedium   *string `json:"utm_medium,omitempty"`
	UTMCampaign *string `json:"utm_campaign,omitempty"`

	// Auto-pause guardrails; see [Campaign.GuardrailEnabled]. Rates are
	// percentages in [0,100] with 0 disabling the rule, GuardrailMinSample is
	// 1-100000 and GuardrailWindowDays 0-365. The tripped-at marker and reason
	// are server-owned and cleared by the next start.
	GuardrailEnabled          *bool    `json:"guardrail_enabled,omitempty"`
	GuardrailBounceRateMax    *float64 `json:"guardrail_bounce_rate_max,omitempty"`
	GuardrailComplaintRateMax *float64 `json:"guardrail_complaint_rate_max,omitempty"`
	GuardrailReplyRateMin     *float64 `json:"guardrail_reply_rate_min,omitempty"`
	GuardrailMinSample        *int     `json:"guardrail_min_sample,omitempty"`
	GuardrailWindowDays       *int     `json:"guardrail_window_days,omitempty"`
}

CampaignUpdateParams updates a campaign. Nil fields are left unchanged, so a zero value is never mistaken for "clear this".

Changing any schedule field (StartDate, EndDate, Timezone, Days, StartTime, EndTime, ScheduleWindows) on an active campaign reschedules its next send immediately. The explicit sender list is not editable here; use CampaignService.ReplaceSenders. Linked segments live under CampaignService.SetSegments.

func (CampaignUpdateParams) MarshalJSON added in v0.3.0

func (p CampaignUpdateParams) MarshalJSON() ([]byte, error)

MarshalJSON emits an explicit null for start_date/end_date when ClearStartDate/ClearEndDate is set, since the API distinguishes an absent field (unchanged) from a null one (cleared).

type CampaignUsage

type CampaignUsage struct {
	Total      int   `json:"total"`
	Active     int   `json:"active"`
	Paused     int   `json:"paused"`
	Draft      int   `json:"draft"`
	EmailsSent int64 `json:"emails_sent"`
}

CampaignUsage counts campaigns by state plus total volume.

type CampaignsOverview

type CampaignsOverview struct {
	Total     int64                 `json:"total"`
	Active    int64                 `json:"active"`
	Paused    int64                 `json:"paused"`
	Draft     int64                 `json:"draft"`
	Completed int64                 `json:"completed"`
	OneTime   int64                 `json:"one_time"`
	Folders   []CampaignFolderCount `json:"folders"`
}

CampaignsOverview backs the campaigns browser: status-bucket counts across the organization plus per-folder totals. Paused sums every paused_* variant; OneTime counts campaigns of CampaignKindOneTime whatever their status.

type CategoryPref

type CategoryPref struct {
	Enabled  bool         `json:"enabled"`
	Channels ChannelPrefs `json:"channels"`
}

CategoryPref is one category's enable flag and channel toggles.

type ChangePlanParams

type ChangePlanParams struct {
	PlanID string `json:"plan_id"`
	// ProrationBehavior controls how the provider settles the switch: one of
	// the Proration* constants.
	ProrationBehavior string `json:"proration_behavior,omitempty"`
	DiscountCode      string `json:"discount_code,omitempty"`
	// Interval is [DurationMonth] or [DurationYear].
	Interval string `json:"interval,omitempty"`
}

ChangePlanParams moves the workspace to a different plan.

type ChannelPrefs

type ChannelPrefs struct {
	InApp bool `json:"in_app"`
	Email bool `json:"email"`
	Slack bool `json:"slack"`
	Push  bool `json:"push"`
}

ChannelPrefs are the delivery toggles for one notification category.

type CheckoutParams

type CheckoutParams struct {
	// PriceID is the payment-provider price to buy.
	PriceID string `json:"price_id"`
	// SuccessURL and CancelURL are where the provider returns the user.
	SuccessURL string `json:"success_url"`
	CancelURL  string `json:"cancel_url"`
	// DiscountCode applies a promotion at checkout.
	DiscountCode string `json:"discount_code,omitempty"`
}

CheckoutParams starts a plan checkout. PriceID, SuccessURL and CancelURL are required.

type CheckoutSession

type CheckoutSession struct {
	SessionID   string `json:"session_id"`
	CheckoutURL string `json:"checkout_url"`
}

CheckoutSession is a hosted checkout to send the user to.

type Client

type Client struct {

	// Emails manages connected mailboxes, their warmup and one-off sends.
	Emails *EmailService
	// Campaigns manages outreach campaigns, their steps and A/B variants.
	Campaigns *CampaignService
	// Contacts manages contacts, their CRM notes and import and export.
	Contacts *ContactService
	// Segments are saved contact audiences evaluated live.
	Segments *SegmentService
	// Suppressions is the workspace's do-not-contact list.
	Suppressions *SuppressionService
	// Forms are hosted lead-capture forms and their submissions.
	Forms *FormService
	// Unibox is the unified inbox: reading, replying and composing.
	Unibox *UniboxService
	// Templates manages reusable reply templates.
	Templates *TemplateService
	// Analytics reads aggregate analytics and deliverability health.
	Analytics *AnalyticsService
	// Advisor reads and acts on continuous checks of the sending posture.
	Advisor *AdvisorService

	// CRM manages pipelines, deals and the task board.
	CRM *CRMService
	// Teams groups members for CRM assignment.
	Teams *TeamService
	// Meetings lists calls booked through a connected scheduler.
	Meetings *MeetingService

	// Integrations manages third-party connections.
	Integrations *IntegrationService
	// Automations manages event-triggered flows across those connections.
	Automations *AutomationService
	// LeadSync manages Google Sheets to contacts sync.
	LeadSync *LeadSyncService

	// Generation writes and rewrites copy with AI.
	Generation *GenerationService
	// Skills manages the workspace AI playbooks that steer it.
	Skills *SkillService
	// Assistant drives the AI assistant and its connected MCP servers.
	Assistant *AssistantService
	// AgentTools is the AI tool registry over plain HTTP, for function-calling
	// agents that do not speak MCP.
	AgentTools *AgentToolService

	// Webhooks manages webhook endpoints and their delivery log.
	Webhooks *WebhookService
	// APIKeys manages API keys.
	APIKeys *APIKeyService
	// OAuthApps registers and manages OAuth 2.1 applications.
	OAuthApps *OAuthAppService

	// Outreach reads and writes the organization-wide sending policy.
	Outreach *OutreachService
	// Deliverability ingests bounce and complaint events from upstream.
	Deliverability *DeliverabilityService
	// WarmupRouting manages warmup partner-selection rules.
	WarmupRouting *WarmupRoutingService
	// Tasks inspects and replays the send-task dead-letter queue.
	Tasks *TaskService
	// AuditLogs reads the organization audit trail.
	AuditLogs *AuditLogService

	// Folders group campaigns, Tags group mailboxes, and Categories group
	// contacts and double as unified-inbox labels.
	Folders    *GroupService
	Tags       *GroupService
	Categories *GroupService

	// Meta reads the caller's identity, the plan catalog and the timezone list.
	Meta *MetaService

	// Auth signs a user in and manages the resulting session. Its routes, and
	// those of Organization and Billing, are session-only: they need a token
	// from [AuthService.Login] rather than an API key.
	Auth *AuthService
	// Organization manages the workspace, its members and its roles.
	Organization *OrganizationService
	// Billing manages the subscription, AI credits and referrals.
	Billing *BillingService
	// WebsiteTracking configures the website tracking snippet (session-only).
	WebsiteTracking *WebsiteTrackingService
	// PoolLink manages self-hosted instances linked to this workspace's warmup
	// pool (session-only).
	PoolLink *PoolLinkService
	// CloudLink is a self-hosted instance's side of the warmup pool link
	// (session-only).
	CloudLink *CloudLinkService
	// contains filtered or unexported fields
}

Client is a Warmbly API client. Create one with New. A Client is safe for concurrent use by multiple goroutines.

Resource groups are exposed as services hanging off the client, for example client.Campaigns and client.APIKeys.

func New

func New(opts ...Option) (*Client, error)

New creates a Client. Exactly one credential option is required: WithAPIKey, WithAccessToken, WithTokenSource or WithAuthenticator.

Example
package main

import (
	"context"
	"fmt"
	"log"

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

func main() {
	client, err := warmbly.New(warmbly.WithAPIKey("wmbly_..."))
	if err != nil {
		log.Fatal(err)
	}

	page, err := client.Campaigns.List(context.Background(), nil)
	if err != nil {
		log.Fatal(err)
	}
	for _, c := range page.Data {
		fmt.Println(c.Name, c.Status)
	}
}

func (*Client) BaseURL

func (c *Client) BaseURL() *url.URL

BaseURL returns the configured API base URL.

func (*Client) Do

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

Do issues a request against an arbitrary API path, decoding a successful JSON body into out (which may be nil). It is the escape hatch for endpoints this SDK release does not model yet.

The path is resolved relative to the base URL, so pass it without the version prefix — "campaigns/123/steps", not "/v1/campaigns/123/steps". Retries, authentication, rate-limit parsing and typed errors work exactly as they do for the typed methods.

type ClientCredentialsConfig

type ClientCredentialsConfig struct {
	ClientID     string
	ClientSecret string
	Scopes       []string
	// TokenURL overrides the token endpoint. Empty uses [DefaultEndpoint].
	TokenURL string
	// EndpointParams are extra parameters added to the token request.
	EndpointParams url.Values
	// HTTPClient is used for token requests. Nil uses a sensible default.
	HTTPClient *http.Client
}

ClientCredentialsConfig configures the OAuth 2.1 client-credentials grant for machine-to-machine access (no user involved).

func (*ClientCredentialsConfig) NewClient

func (c *ClientCredentialsConfig) NewClient(ctx context.Context, opts ...Option) (*Client, error)

NewClient builds a *Client authenticated with the client-credentials grant.

func (*ClientCredentialsConfig) Token

Token fetches a new access token using the client-credentials grant.

func (*ClientCredentialsConfig) TokenSource

func (c *ClientCredentialsConfig) TokenSource(ctx context.Context) TokenSource

TokenSource returns a TokenSource that fetches and caches a token, renewing it as it expires. It is safe for concurrent use.

type CloudLinkConnectPollResult added in v0.3.0

type CloudLinkConnectPollResult struct {
	// Status is [PoolLinkCodePending] or [PoolLinkCodeApproved]; a denied,
	// expired or forgotten handshake is reported as an error.
	Status string `json:"status"`
	// Link is the stored connection, set once approved.
	Link *CloudLinkConnection `json:"link,omitempty"`
	// Info is the cloud's status document, set once approved when the cloud
	// answered the follow-up call.
	Info *PoolLinkInstanceInfo `json:"info,omitempty"`
}

CloudLinkConnectPollResult is one answer to CloudLinkService.PollConnect.

type CloudLinkConnection added in v0.3.0

type CloudLinkConnection struct {
	// CloudURL is the Warmbly Cloud API the instance is linked to.
	CloudURL string `json:"cloud_url"`
	// InstanceID is the instance's ID on the cloud, as
	// [PoolLinkInstance.ID] on the workspace's side.
	InstanceID string `json:"instance_id"`
	// OrganizationName is the cloud workspace's name at link time.
	OrganizationName string `json:"organization_name"`
	// ConnectedBy is the local member who ran the handshake.
	ConnectedBy *string   `json:"connected_by,omitempty"`
	ConnectedAt time.Time `json:"connected_at"`
	// LastSyncedAt is the last time the instance reached the cloud, and
	// LastError the failure from the most recent attempt, if it failed.
	LastSyncedAt *time.Time `json:"last_synced_at,omitempty"`
	LastError    string     `json:"last_error,omitempty"`
}

CloudLinkConnection is the instance's link to a Warmbly Cloud workspace. The instance token is never serialized.

type CloudLinkMailbox added in v0.3.0

type CloudLinkMailbox struct {
	// ID is the mailbox's ID on this instance, as [Email.ID].
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name"`
	// Provider is [ProviderGmail], [ProviderOutlook] or [ProviderSMTPIMAP].
	Provider string `json:"provider"`
	// Status is the local connection state, for example [MailboxStatusActive].
	Status string `json:"status"`
	// Enrolled reports whether the mailbox warms in the hosted pool; the
	// instance's own warmup stands down for it while it does.
	Enrolled   bool       `json:"enrolled"`
	EnrolledAt *time.Time `json:"enrolled_at,omitempty"`
	// Managed is true for a mailbox signed in through Warmbly Cloud: the
	// credential lives on the cloud and the instance sends with brokered
	// tokens.
	Managed bool `json:"managed"`
	// Cloud is the cloud's view of an enrolled mailbox (health, today's
	// count, 7-day totals). Nil when not enrolled or when the cloud could not
	// be reached for the listing.
	Cloud *PoolLinkMailboxState `json:"cloud,omitempty"`
}

CloudLinkMailbox is one of the instance's active mailboxes with its pool enrollment, as listed on the Warmbly Cloud settings page.

type CloudLinkOAuthStart added in v0.3.0

type CloudLinkOAuthStart struct {
	URL string `json:"url"`
	// Session is single-use and expires after about 15 minutes.
	Session string `json:"session"`
}

CloudLinkOAuthStart is the handle on a Google or Microsoft consent brokered by the cloud: open URL for the operator, then redeem Session with CloudLinkService.FinishOAuth once the window returns.

type CloudLinkPendingConnect added in v0.3.0

type CloudLinkPendingConnect struct {
	// UserCode is the short code the operator types in on Warmbly Cloud.
	UserCode string `json:"user_code"`
	// VerificationURL is the cloud page that approves the code; it already
	// carries the user code as a query parameter.
	VerificationURL string `json:"verification_url"`
	// CloudURL is the cloud the handshake was opened against.
	CloudURL string `json:"cloud_url"`
	// ExpiresAt is when an unapproved code is retired.
	ExpiresAt time.Time `json:"expires_at"`
	// Interval is the minimum number of seconds between polls.
	Interval int `json:"interval"`
}

CloudLinkPendingConnect is an in-flight handshake: what to show the operator and how to poll. It lives in the instance's memory only, so a restart forgets it.

type CloudLinkService added in v0.3.0

type CloudLinkService service

CloudLinkService is a self-hosted instance's side of the warmup pool link, the "Settings > Warmbly Cloud" page: connect the instance to a Warmbly Cloud workspace, choose which local mailboxes warm in the hosted pool, and add Google or Microsoft mailboxes through Warmbly's own OAuth apps.

Only warmup moves to the cloud. Campaigns, contacts and the inbox stay on the instance. The link itself is a property of the instance, not of a workspace, so an instance holds at most one.

Connecting is a device-code handshake driven from here: CloudLinkService.Connect asks the cloud for a code and returns what to show the operator, who approves it on Warmbly Cloud; meanwhile CloudLinkService.WaitForConnect polls until the instance has its token.

Every route is session-only (a token from AuthService.Login, never an API key) and needs an active workspace on the session. Reads are open to any member, since no secret travels; connecting, polling and disconnecting are a settings change and need the manage-settings permission; enrolling, pausing, resuming, adopting and the OAuth sign-in are mailbox changes and need the manage-emails permission.

On a deployment without the cloud link (the hosted product itself, or an instance built without it) every route answers 501 Not Implemented.

func (*CloudLinkService) AdoptWorkspaceMailbox added in v0.3.0

func (s *CloudLinkService) AdoptWorkspaceMailbox(ctx context.Context, id string, opts ...RequestOption) (*Email, *Response, error)

AdoptWorkspaceMailbox adds cloud workspace mailbox id (PoolLinkWorkspaceMailbox.ID) to the instance the same way as CloudLinkService.FinishOAuth: the cloud keeps the sign-in, the instance gets a credential-free mirror, returned here. A workspace mailbox can be linked to one instance at a time; a second adoption fails with "pool_link_already_adopted". Needs the manage-emails permission; recorded in the audit log.

func (*CloudLinkService) Connect added in v0.3.0

func (s *CloudLinkService) Connect(ctx context.Context, cloudURL string, opts ...RequestOption) (*CloudLinkPendingConnect, *Response, error)

Connect opens the handshake against cloudURL ("" for the instance's default, see CloudLinkStatus.DefaultCloudURL) and returns the code to show the operator. Follow up with CloudLinkService.WaitForConnect.

cloudURL must be https (loopback http is allowed for local development). An instance that is already linked fails with "cloud_link_connected": disconnect first. Needs the manage-settings permission.

func (*CloudLinkService) Disconnect added in v0.3.0

func (s *CloudLinkService) Disconnect(ctx context.Context, opts ...RequestOption) (*Response, error)

Disconnect ends the link. Every enrolled mailbox leaves the pool, their credentials are deleted on the cloud and local warmup takes over; mailboxes signed in through Warmbly Cloud are removed from the instance altogether, since they cannot send without the link (they stay in the cloud workspace). Needs the manage-settings permission; recorded in the audit log.

The cloud must confirm (or already have dropped) the link before local state goes, so a cloud outage fails the call rather than leaving managed mailboxes orphaned there; retry later.

func (*CloudLinkService) EnrollMailbox added in v0.3.0

func (s *CloudLinkService) EnrollMailbox(ctx context.Context, id string, opts ...RequestOption) (*CloudLinkMailbox, *Response, error)

EnrollMailbox sends mailbox id's SMTP/IMAP credential to the cloud and starts warming it in the hosted pool with the ramp settings it has on the instance; local warmup stands down for it. Idempotent for an enrolled mailbox. Needs the manage-emails permission; recorded in the audit log.

Only active SMTP/IMAP mailboxes qualify: one signed in with the instance's own Google or Microsoft OAuth app fails with "cloud_link_oauth_mailbox" (re-add it through CloudLinkService.StartOAuth instead), an inactive one with "cloud_link_mailbox_inactive", and one over the workspace's allowance with "pool_link_mailbox_limit".

func (*CloudLinkService) FinishOAuth added in v0.3.0

func (s *CloudLinkService) FinishOAuth(ctx context.Context, session string, opts ...RequestOption) (*Email, *Response, error)

FinishOAuth redeems a completed consent. The mailbox is created in the cloud workspace, where its sign-in lives, and starts warming in the pool at once; the instance gets a credential-free mirror of it, returned here, which campaigns and the inbox use through brokered access tokens. Needs the manage-emails permission; recorded in the audit log.

The session must belong to the session's workspace and is single-use; an unknown or expired one fails with "cloud_link_oauth_session", a consent the operator has not finished yet with "pool_link_oauth_pending".

func (*CloudLinkService) ListMailboxes added in v0.3.0

func (s *CloudLinkService) ListMailboxes(ctx context.Context, opts ...RequestOption) ([]CloudLinkMailbox, *Response, error)

ListMailboxes lists every active mailbox in the session's workspace with its pool enrollment and, for enrolled ones, the cloud's view. Open to any member. The listing makes one round trip to the cloud; when that fails the rows still come back, with CloudLinkMailbox.Cloud nil.

func (*CloudLinkService) ListWorkspaceMailboxes added in v0.3.0

func (s *CloudLinkService) ListWorkspaceMailboxes(ctx context.Context, opts ...RequestOption) ([]PoolLinkWorkspaceMailbox, *Response, error)

ListWorkspaceMailboxes lists the Google and Microsoft mailboxes connected directly on the linked cloud workspace that this instance may adopt. Open to any member; fails with "cloud_link_not_connected" without a link.

func (*CloudLinkService) PauseMailbox added in v0.3.0

func (s *CloudLinkService) PauseMailbox(ctx context.Context, id string, opts ...RequestOption) (*CloudLinkMailbox, *Response, error)

PauseMailbox pauses pool warmup for enrolled mailbox id without unenrolling it. Needs the manage-emails permission; recorded in the audit log. A mailbox that is not enrolled fails with not found.

func (*CloudLinkService) PollConnect added in v0.3.0

PollConnect asks the cloud once whether the pending code has been approved, and on approval stores the instance token and returns the new link. Needs the manage-settings permission.

With no handshake in progress it fails with "cloud_link_no_pending" (or, if another session finished the handshake meanwhile, answers approved with the stored link); an expired code with "cloud_link_code_expired"; a code the member declined with "pool_link_denied". Wait at least CloudLinkPendingConnect.Interval seconds between calls.

func (*CloudLinkService) ResumeMailbox added in v0.3.0

func (s *CloudLinkService) ResumeMailbox(ctx context.Context, id string, opts ...RequestOption) (*CloudLinkMailbox, *Response, error)

ResumeMailbox resumes pool warmup for a paused mailbox. Needs the manage-emails permission; recorded in the audit log.

func (*CloudLinkService) StartOAuth added in v0.3.0

func (s *CloudLinkService) StartOAuth(ctx context.Context, provider string, opts ...RequestOption) (*CloudLinkOAuthStart, *Response, error)

StartOAuth begins a Google or Microsoft sign-in on Warmbly's own OAuth apps, so the instance needs no client of its own. provider is ProviderGmail or ProviderOutlook. Open the returned URL for the operator; the consent window returns to the instance's dashboard, which then calls CloudLinkService.FinishOAuth with the session. Needs the manage-emails permission and a live link.

func (*CloudLinkService) Status added in v0.3.0

Status reports whether the instance is linked, the cloud's view of it when reachable, and the default cloud URL. Open to any member.

func (*CloudLinkService) UnenrollMailbox added in v0.3.0

func (s *CloudLinkService) UnenrollMailbox(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

UnenrollMailbox takes mailbox id out of the pool. An SMTP/IMAP mailbox's credential is deleted on the cloud and local warmup takes over; a mailbox signed in through Warmbly Cloud is removed from the instance instead and stays in the cloud workspace. Succeeds for a mailbox that is not enrolled. Needs the manage-emails permission; recorded in the audit log.

func (*CloudLinkService) WaitForConnect added in v0.3.0

WaitForConnect polls every CloudLinkPendingConnect.Interval seconds (never faster than once a second) until the operator approves the code, the instance reports a terminal error, or ctx is done. Once approved the link is stored on the instance and the result carries it.

The cloud retires an unapproved code at CloudLinkPendingConnect.ExpiresAt, after which the loop ends with a "cloud_link_code_expired" error; bound ctx yourself to give up sooner.

type CloudLinkStatus added in v0.3.0

type CloudLinkStatus struct {
	// Connected reports whether the instance holds a link at all. Link is
	// filled from the local row whenever it does.
	Connected bool                 `json:"connected"`
	Link      *CloudLinkConnection `json:"link,omitempty"`
	// Info is the cloud's own status document for this instance, including
	// the pool allowance; it is set only when Reachable.
	Info *PoolLinkInstanceInfo `json:"info,omitempty"`
	// Reachable is false when the cloud could not be contacted just now;
	// Error then carries the reason and Link still comes from the local row.
	Reachable bool   `json:"reachable"`
	Error     string `json:"error,omitempty"`
	// DefaultCloudURL is the cloud [CloudLinkService.Connect] proposes when
	// given no URL: WARMBLY_CLOUD_URL or the hosted API.
	DefaultCloudURL string `json:"default_cloud_url"`
}

CloudLinkStatus is the dashboard's view of the link.

type ColdRamp added in v0.3.0

type ColdRamp struct {
	// Ceiling is today's cold allowance; MailboxCap is what the owner
	// configured.
	Ceiling    int `json:"ceiling"`
	MailboxCap int `json:"mailbox_cap"`
	// DaysToFullCap is how many clean days remain before Ceiling reaches
	// MailboxCap, 0 when it arrives today.
	DaysToFullCap int `json:"days_to_full_cap"`
	// Held is true when a recent spam placement is pausing the climb.
	Held bool `json:"held"`
}

ColdRamp explains a cold sending cap held below the mailbox's configured campaign limit while it graduates from warmup.

type ComposeCandidate

type ComposeCandidate struct {
	ID       string `json:"id"`
	Email    string `json:"email"`
	Name     string `json:"name"`
	Provider string `json:"provider"`
	// AuthState is the sending domain's SPF/DKIM/DMARC verdict.
	AuthState    string `json:"auth_state"`
	WarmupActive bool   `json:"warmup_active"`
	DailyLimit   int    `json:"daily_limit"`
	SentToday    int    `json:"sent_today"`
	// RemainingToday is how much of the daily allowance is left.
	RemainingToday int `json:"remaining_today"`
	// HistoryMessages counts prior traffic between this mailbox and the
	// recipient, which is the strongest affinity signal.
	HistoryMessages int        `json:"history_messages"`
	LastContactAt   *time.Time `json:"last_contact_at,omitempty"`
	// Score ranks the candidate; Reasons explains the ranking in words.
	Score       int      `json:"score"`
	Reasons     []string `json:"reasons"`
	Recommended bool     `json:"recommended"`
}

ComposeCandidate is one mailbox scored as a sender for a recipient.

type ComposeCandidates

type ComposeCandidates struct {
	Accounts []ComposeCandidate `json:"accounts"`
	// RecommendedAccountID is what automatic selection would pick.
	RecommendedAccountID *string `json:"recommended_account_id"`
	RecommendedReason    string  `json:"recommended_reason"`
	// Contact is the resolved contact for the address, when there is one.
	Contact *Contact `json:"contact"`
	// Suppression is non-nil when the address is suppressed workspace-wide, in
	// which case a compose to it will be rejected.
	Suppression *ComposeSuppression `json:"suppression"`
}

ComposeCandidates is the compose mailbox picker: every active mailbox scored against the recipient, the automatic recommendation and why, plus what is known about the address.

type ComposeDraft

type ComposeDraft struct {
	ID             string    `json:"id"`
	EmailAccountID *string   `json:"email_account_id,omitempty"`
	To             []string  `json:"to"`
	CC             []string  `json:"cc"`
	BCC            []string  `json:"bcc"`
	Subject        string    `json:"subject"`
	Body           string    `json:"body"`
	UpdatedAt      time.Time `json:"updated_at"`
	CreatedAt      time.Time `json:"created_at"`
}

ComposeDraft is an autosaved compose draft. Ids are client-generated, which makes the upsert idempotent under debounced autosave.

type ComposeDraftParams

type ComposeDraftParams struct {
	// EmailAccountID is the chosen mailbox; empty or "auto" stores none.
	EmailAccountID string   `json:"email_account_id,omitempty"`
	To             []string `json:"to,omitempty"`
	CC             []string `json:"cc,omitempty"`
	BCC            []string `json:"bcc,omitempty"`
	Subject        string   `json:"subject,omitempty"`
	Body           string   `json:"body,omitempty"`
}

ComposeDraftParams is the body of an autosave. The server rejects a draft over 100,000 body bytes, 1,000 subject bytes or 100 recipients per field.

type ComposeResult added in v0.3.0

type ComposeResult struct {
	SendResult
	// AccountID and AccountEmail identify the sending mailbox.
	AccountID    string `json:"account_id"`
	AccountEmail string `json:"account_email"`
	// Auto is true when the server chose the mailbox, in which case
	// PickedReason explains the choice in words.
	Auto         bool   `json:"auto"`
	PickedReason string `json:"picked_reason,omitempty"`
}

ComposeResult is returned when a composed message has been accepted for delivery. Beyond the queued send it reports which mailbox was used, which matters when the server picked it.

type ComposeSuppression

type ComposeSuppression struct {
	Reason string `json:"reason"`
}

ComposeSuppression explains why an address cannot be mailed.

type ConfirmParams

type ConfirmParams struct {
	// Session is the handle returned by the first step.
	Session   string `json:"session"`
	Code      string `json:"code"`
	Turnstile string `json:"turnstile,omitempty"`
}

ConfirmParams completes a two-step flow with the emailed code.

type ConnectParams

type ConnectParams struct {
	// Provider is one of the Provider* constants.
	Provider string `json:"provider"`
	Label    string `json:"label,omitempty"`
	// Config carries the provider's credentials, which are sealed server-side.
	Config map[string]any `json:"config,omitempty"`
}

ConnectParams connects a provider that authenticates with an API key or a webhook URL. Use IntegrationService.StartOAuth for OAuth providers.

type ConnectionConfigParams

type ConnectionConfigParams struct {
	ConfigCapabilities json.RawMessage `json:"config_capabilities,omitempty"`
	// SyncDirection is [SyncPush], [SyncPull] or [SyncBoth].
	SyncDirection string `json:"sync_direction,omitempty"`
}

ConnectionConfigParams updates a connection's non-secret configuration.

type ConnectionDetail

type ConnectionDetail struct {
	Connection *IntegrationConnection `json:"connection"`
	Events     []EventSubscription    `json:"events"`
	Runs       []SyncRun              `json:"runs"`
}

ConnectionDetail is a connection together with its event subscriptions and recent sync runs.

type ConnectionWebhookSecret

type ConnectionWebhookSecret struct {
	SigningSecret   string `json:"signing_secret"`
	SignatureHeader string `json:"signature_header"`
	// Scheme names the signing algorithm the provider should use.
	Scheme string `json:"scheme"`
}

ConnectionWebhookSecret is what an inbound provider needs to sign its callbacks to Warmbly.

type ConsentInfo

type ConsentInfo struct {
	ClientID    string `json:"client_id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	LogoURL     string `json:"logo_url"`
	WebsiteURL  string `json:"website_url"`
	RedirectURI string `json:"redirect_uri"`
	// Scopes are the human-readable scope names being requested.
	Scopes []string `json:"scopes"`
	State  string   `json:"state"`
}

ConsentInfo is what a consent screen renders: who is asking, for what, and where the user will be sent back.

type Contact

type Contact struct {
	ID string `json:"id"`

	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Email     string `json:"email"`
	Company   string `json:"company"`
	Phone     string `json:"phone"`

	CustomFields map[string]string `json:"custom_fields"`

	// Subscribed is the marketing-consent flag. Campaigns never send to an
	// unsubscribed contact.
	Subscribed bool           `json:"subscribed"`
	Campaigns  []MiniCampaign `json:"campaigns"`
	Categories []MiniCategory `json:"categories"`

	// VerificationStatus is the pre-send address check: [VerifyStatusValid],
	// [VerifyStatusRisky], [VerifyStatusInvalid] or [VerifyStatusUnknown].
	// Campaigns never send to invalid addresses, and send to risky ones only
	// when their risky-emails setting is on.
	VerificationStatus string `json:"verification_status"`
	// VerificationReason is a short explanation of the verdict.
	VerificationReason string `json:"verification_reason"`
	// VerificationSubStatus refines the status: one of the
	// VerificationSubStatus* constants, or empty.
	VerificationSubStatus string `json:"verification_sub_status"`
	// VerificationSource says who produced the verdict: one of the
	// VerificationSource* constants, or empty when never checked.
	VerificationSource string `json:"verification_source"`
	// VerificationProvider names the verifier or vocabulary behind the
	// verdict, for example [VerificationProviderBuiltin] or
	// [VerificationProviderZeroBounce].
	VerificationProvider string `json:"verification_provider"`
	// VerificationConfidence is 0 to 100, scored from the last check plus what
	// real mail to the address showed (deliveries, opens, replies, bounces).
	VerificationConfidence int        `json:"verification_confidence"`
	IsCatchAll             bool       `json:"is_catch_all"`
	VerificationCheckedAt  *time.Time `json:"verification_checked_at,omitempty"`

	// ESPProvider is the recipient's mail provider, derived from the domain:
	// "", "gmail", "outlook" or "other". Campaign ESP matching keys off it.
	ESPProvider   string     `json:"esp_provider"`
	ESPResolvedAt *time.Time `json:"esp_resolved_at,omitempty"`

	// CampaignLead is the contact's state within a single campaign. It is
	// populated only when a search filters by exactly one campaign.
	CampaignLead *ContactCampaignProgress `json:"campaign_lead,omitempty"`

	UpdatedAt time.Time `json:"updated_at"`
	CreatedAt time.Time `json:"created_at"`
}

Contact is a contact (lead) as returned by the API.

type ContactActivity

type ContactActivity struct {
	ID             string  `json:"id"`
	ContactID      string  `json:"contact_id"`
	OrganizationID string  `json:"organization_id"`
	UserID         *string `json:"user_id,omitempty"`
	// ActivityType is one of the Activity* constants.
	ActivityType string         `json:"activity_type"`
	Metadata     map[string]any `json:"metadata"`
	CreatedAt    time.Time      `json:"created_at"`
	User         *User          `json:"user,omitempty"`
}

ContactActivity is one machine-recorded event on a contact's CRM record.

type ContactBulkUpdateParams

type ContactBulkUpdateParams struct {
	// Contacts are the contact ids to edit, at most 1,000 per request.
	Contacts []string `json:"contacts"`

	AddCampaigns     []string           `json:"add_campaigns,omitempty"`
	RemoveCampaigns  []string           `json:"remove_campaigns,omitempty"`
	AddCategories    []string           `json:"add_categories,omitempty"`
	RemoveCategories []string           `json:"remove_categories,omitempty"`
	Fields           []ContactFieldEdit `json:"fields,omitempty"`
	// Subscribe sets the subscription flag on every listed contact.
	Subscribe *bool `json:"subscribe,omitempty"`
}

ContactBulkUpdateParams edits many contacts at once.

type ContactCampaignProgress

type ContactCampaignProgress struct {
	// Status is one of the LeadStatus* constants.
	Status string `json:"status"`
	// Sent counts steps the worker delivered to the mailbox provider; a send
	// that could not complete is retried later and never shows here.
	Sent int `json:"sent"`
	// Opened counts steps opened by a person. Automated fetches (mail privacy
	// proxies and similar) are counted in MachineOpened instead.
	Opened         int        `json:"opened"`
	MachineOpened  int        `json:"machine_opened"`
	Clicked        int        `json:"clicked"`
	Replied        int        `json:"replied"`
	Bounced        int        `json:"bounced"`
	LastActivityAt *time.Time `json:"last_activity_at,omitempty"`
	// CurrentStep labels the latest step actually sent. It is empty while the
	// status is [LeadStatusPending].
	CurrentStep string `json:"current_step,omitempty"`
	// FailureReason is the sending worker's reason for the last failed send.
	// Set only when Status is [LeadStatusFailed].
	FailureReason string `json:"failure_reason,omitempty"`
}

ContactCampaignProgress is a contact's aggregate state inside one campaign.

type ContactCampaignState added in v0.3.0

type ContactCampaignState struct {
	CampaignID     string `json:"campaign_id"`
	CampaignName   string `json:"campaign_name"`
	CampaignStatus string `json:"campaign_status"`
	// LeadStatus is one of the LeadStatus* constants.
	LeadStatus string `json:"lead_status"`
	// FailureReason is the worker's reason for the last failed send.
	FailureReason string `json:"failure_reason,omitempty"`

	Steps          []ContactCampaignStep `json:"steps"`
	CompletedSteps int                   `json:"completed_steps"`
	TotalSteps     int                   `json:"total_steps"`

	// CurrentStep is the latest step sent.
	CurrentStep  *ContactCampaignStep `json:"current_step,omitempty"`
	LastAction   string               `json:"last_action,omitempty"`
	LastActionAt *time.Time           `json:"last_action_at,omitempty"`

	// Next is nil once the flow has ended for the contact; EndedReason says
	// why.
	Next        *ContactNextAction `json:"next,omitempty"`
	EndedReason string             `json:"ended_reason,omitempty"`
}

ContactCampaignState is one campaign a contact is a lead of: the flow with the contact's progress on each step, the derived lead status, the last thing that happened and what happens next.

type ContactCampaignStep added in v0.3.0

type ContactCampaignStep struct {
	ID       string `json:"id"`
	Label    string `json:"label"`
	Kind     string `json:"kind"`
	Position int    `json:"position"`
	Subject  string `json:"subject,omitempty"`

	SentAt    *time.Time `json:"sent_at,omitempty"`
	OpenedAt  *time.Time `json:"opened_at,omitempty"`
	ClickedAt *time.Time `json:"clicked_at,omitempty"`
	RepliedAt *time.Time `json:"replied_at,omitempty"`
	BouncedAt *time.Time `json:"bounced_at,omitempty"`
	FailedAt  *time.Time `json:"failed_at,omitempty"`
	// Attempts counts failed sends; InFlight means a worker holds a
	// reservation whose result has not come back yet.
	Attempts int  `json:"attempts,omitempty"`
	InFlight bool `json:"in_flight,omitempty"`
}

ContactCampaignStep is one flow node with the contact's progress on it.

type ContactCategoryCount

type ContactCategoryCount struct {
	CategoryID string `json:"category_id"`
	Count      int    `json:"count"`
}

ContactCategoryCount is how many contacts carry one category.

type ContactCreatedPayload added in v0.3.0

type ContactCreatedPayload struct {
	ContactID string `json:"contact_id"`
	// ContactEmail is the address, named for the template variable rather than
	// the column.
	ContactEmail string `json:"contact_email"`
	FirstName    string `json:"first_name,omitempty"`
	LastName     string `json:"last_name,omitempty"`
	Company      string `json:"company,omitempty"`
	Phone        string `json:"phone,omitempty"`
	// Subscribed is the marketing-consent flag. A new contact defaults to
	// true unless the creating call said otherwise.
	Subscribed bool `json:"subscribed"`
	// CustomFields is the contact's custom column values, empty rather than
	// null when it has none.
	CustomFields map[string]string `json:"custom_fields,omitempty"`
	// Source is the first-touch origin, one of the ContactSource* constants,
	// and SourceDetail names the specific origin: the file name for an import,
	// the API key's name for an API write, the form for a submission.
	Source       string `json:"source"`
	SourceDetail string `json:"source_detail,omitempty"`
	// CampaignIDs and CategoryIDs are the campaigns and lists the contact was
	// created into, when it was created into any.
	CampaignIDs []string  `json:"campaign_ids,omitempty"`
	CategoryIDs []string  `json:"category_ids,omitempty"`
	CreatedAt   time.Time `json:"created_at"`
}

ContactCreatedPayload is the EventContactCreated body: the contact's own fields plus where it came from, flat, so an automation template can read {{.contact_email}} without digging into a nested object.

It does not fire for every row that lands in the workspace. A write that matched an existing contact is an update, not a creation, and stays silent; so does a bulk arrival — a file import or a sheet sync — and so does any single write of more than 100 contacts, on the grounds that one upload should not flood an endpoint with thousands of deliveries. Reconcile bulk arrivals from the bulk_operation events or a list refetch instead.

type ContactDetail

type ContactDetail struct {
	Contact
	Engagement  ContactEngagement   `json:"engagement"`
	Suppression *ContactSuppression `json:"suppression,omitempty"`
	// Verification explains the verdict: the reasons behind it and the
	// observations it was scored from.
	Verification *ContactVerificationDetail `json:"verification,omitempty"`

	// Source is one of the ContactSource* constants; SourceDetail names the
	// file, campaign, sheet, form, automation or API key behind it. Both are
	// fixed at creation.
	Source       string    `json:"source"`
	SourceDetail string    `json:"source_detail"`
	FirstSeenAt  time.Time `json:"first_seen_at"`
}

ContactDetail is the hydrated contact 360 view returned by ContactService.Get: the contact plus its engagement rollup, suppression state, verification explanation and first-touch attribution in one payload.

type ContactEngagement

type ContactEngagement struct {
	TotalSent       int `json:"total_sent"`
	TotalOpened     int `json:"total_opened"`
	TotalClicked    int `json:"total_clicked"`
	TotalReplied    int `json:"total_replied"`
	TotalBounced    int `json:"total_bounced"`
	TotalComplained int `json:"total_complained"`

	LastSentAt    *time.Time `json:"last_sent_at,omitempty"`
	LastOpenedAt  *time.Time `json:"last_opened_at,omitempty"`
	LastClickedAt *time.Time `json:"last_clicked_at,omitempty"`
	LastRepliedAt *time.Time `json:"last_replied_at,omitempty"`
	LastBouncedAt *time.Time `json:"last_bounced_at,omitempty"`
}

ContactEngagement summarizes every email touchpoint recorded for a contact. Opens count people only: fetches by a mail client or security gateway are left out, as they are in campaign analytics. A person's click counts as an open too.

type ContactExportParams

type ContactExportParams struct {
	// Format is [ExportFormatCSV], [ExportFormatXLSX] or [ExportFormatJSON].
	Format string `json:"format"`
	// Scope is [ExportScopeAll], [ExportScopeFiltered] or [ExportScopeSelected].
	Scope string `json:"scope"`
	// ContactIDs is required for [ExportScopeSelected].
	ContactIDs []string `json:"contact_ids,omitempty"`
	// Filters is required for [ExportScopeFiltered]. With
	// [ExportScopeSelected] it is optional and applied on top of ContactIDs;
	// name the campaign there to populate the ExportFieldLead* columns.
	Filters *ContactSearchParams `json:"filters,omitempty"`
	// Fields are column identifiers in display order. Empty means the
	// recommended default set.
	Fields []string `json:"fields,omitempty"`
	// Filename is the download name without an extension. Empty falls back to
	// "contacts-<date>".
	Filename string `json:"filename,omitempty"`
}

ContactExportParams describes an export. A single export is capped at 50,000 rows server-side.

type ContactFieldEdit

type ContactFieldEdit struct {
	// Type is one of the FieldOp* constants.
	Type  string `json:"type"`
	Key   string `json:"key"`
	Value string `json:"value,omitempty"`
}

ContactFieldEdit is one custom-field change in a bulk update.

type ContactFieldFilter

type ContactFieldFilter struct {
	Name  string `json:"name"`
	Value string `json:"value"`
	// Type is one of the Filter* constants.
	Type string `json:"type"`
}

ContactFieldFilter matches one custom field.

type ContactImportParams

type ContactImportParams struct {
	Mapping []ImportColumnMapping `json:"mapping"`
	// Dedup is one of the ImportDedup* constants.
	Dedup     string `json:"dedup,omitempty"`
	HasHeader bool   `json:"has_header"`
	// CategoryIDs and CampaignIDs are applied to every imported contact.
	CategoryIDs []string `json:"category_ids,omitempty"`
	CampaignIDs []string `json:"campaign_ids,omitempty"`
	// SegmentIDs pins every row into these segments as a manual include
	// override: imported, updated and skipped-but-matched contacts alike.
	SegmentIDs []string `json:"segment_ids,omitempty"`
	// SubscribedDefault is what new contacts inherit when no subscribed column
	// was mapped. It defaults to true server-side.
	SubscribedDefault *bool `json:"subscribed_default,omitempty"`
}

ContactImportParams is the configuration committed alongside the file.

Exactly one column must map to ImportTargetEmail. A mapping with no email column, a custom column with no CustomKey, or a custom key with characters outside letters, numbers, underscores, spaces and dashes is a 400 on the whole request, raised before any row is written.

type ContactImportPreview

type ContactImportPreview struct {
	Filename  string `json:"filename"`
	Format    string `json:"format"`
	TotalRows int    `json:"total_rows"`
	// Columns are the detected headers. For a headerless file the server
	// synthesizes "Column 1", "Column 2" and so on.
	Columns   []string `json:"columns"`
	HasHeader bool     `json:"has_header"`
	// SampleRows are the first rows verbatim.
	SampleRows [][]string `json:"sample_rows"`
	// SuggestedMapping is a heuristic default the caller may override. It
	// proposes [ImportTargetVerificationStatus] itself when a column's header
	// or values look like another service's results.
	SuggestedMapping []ImportColumnMapping `json:"suggested_mapping"`
}

ContactImportPreview describes an uploaded file before anything is written.

type ContactImportQuality added in v0.3.0

type ContactImportQuality struct {
	// Malformed rows are not addresses at all; Disposable are on known
	// throwaway domains.
	Malformed  int `json:"malformed"`
	Disposable int `json:"disposable"`
	// Role counts shared inboxes such as info@. Reported but not counted as
	// bad, since mailing a shared inbox is a choice.
	Role int `json:"role"`
	// BadSharePct is malformed plus disposable as a percentage of the file.
	BadSharePct float64 `json:"bad_share_pct"`
	// Flagged is set above 25% on files of at least 20 rows, with a Summary
	// sentence.
	Flagged bool   `json:"flagged"`
	Summary string `json:"summary,omitempty"`
}

ContactImportQuality is what the uploaded addresses looked like, measured at import. It is advisory: a flagged import still stores every row it could parse. A list bad enough to matter is refused at campaign launch instead.

type ContactImportResult

type ContactImportResult struct {
	Total     int       `json:"total"`
	Imported  int       `json:"imported"`
	Updated   int       `json:"updated"`
	Skipped   int       `json:"skipped"`
	Failed    int       `json:"failed"`
	StartedAt time.Time `json:"started_at"`
	EndedAt   time.Time `json:"ended_at"`
	// Errors holds at most the first 1,000 per-row failures. Past that
	// ErrorsTruncated is true and the counters, not the list, are the real
	// totals.
	Errors          []ImportRowError `json:"errors,omitempty"`
	ErrorsTruncated bool             `json:"errors_truncated,omitempty"`
	// Quality is the file's address-level assessment.
	Quality *ContactImportQuality `json:"quality,omitempty"`
}

ContactImportResult summarizes a committed import. Every row lands in exactly one of Imported, Updated, Skipped or Failed, so those always sum to Total.

type ContactInput

type ContactInput struct {
	Email     string `json:"email"`
	FirstName string `json:"first_name,omitempty"`
	LastName  string `json:"last_name,omitempty"`
	Company   string `json:"company,omitempty"`
	Phone     string `json:"phone,omitempty"`
	// Campaigns and Categories are ids to enroll the new contact in.
	Campaigns  []string `json:"campaigns,omitempty"`
	Categories []string `json:"categories,omitempty"`
	// Segments pins the contact into these segments as a manual include
	// override, so it belongs whether or not the conditions match it. An
	// unknown id is a 400 before any contact is written.
	Segments []string `json:"segments,omitempty"`
	// CustomFields keys may use letters, numbers, underscores, spaces and
	// dashes.
	CustomFields map[string]string `json:"custom_fields,omitempty"`
	// Subscribed is the marketing-consent flag. Nil lets a new contact default
	// to subscribed and an existing one keep what it had.
	Subscribed *bool `json:"subscribed,omitempty"`
	// VerificationStatus is a verdict you already hold for the address, in
	// Warmbly's vocabulary (the VerifyStatus* constants) or any known
	// service's ("ok", "catch-all", "do_not_mail", "deliverable", ...). It is
	// stored as an imported verdict the background check leaves alone. A
	// value no known service writes is rejected with code
	// "unknown_verification_status".
	VerificationStatus string `json:"verification_status,omitempty"`
	// VerificationProvider names the vocabulary VerificationStatus is written
	// in (a VerificationProvider* constant). Empty recognizes the value by
	// itself; an unknown name is rejected with "unknown_verification_provider".
	VerificationProvider string `json:"verification_provider,omitempty"`
	// Source is the first-touch attribution stamped on a new contact. Only
	// [ContactSourceManual] and [ContactSourceCampaign] may be claimed, and
	// only by a user-scoped (OAuth) caller; a request authenticated with an
	// API key is always recorded as [ContactSourceAPI] under the key's name.
	// An existing contact keeps its original source.
	Source string `json:"source,omitempty"`
}

ContactInput creates one contact. Email is required.

An address the organization already has is matched (case-insensitively) and enriched rather than duplicated: fields you send replace what is stored, fields you omit or leave empty are kept, and CustomFields is merged key by key. Use ContactService.Update to clear a value.

type ContactLinkClick added in v0.3.0

type ContactLinkClick struct {
	ID          string `json:"id"`
	URL         string `json:"url"`
	Label       string `json:"label,omitempty"`
	UTMSource   string `json:"utm_source,omitempty"`
	UTMMedium   string `json:"utm_medium,omitempty"`
	UTMCampaign string `json:"utm_campaign,omitempty"`
	UTMTerm     string `json:"utm_term,omitempty"`
	UTMContent  string `json:"utm_content,omitempty"`
	UserAgent   string `json:"user_agent,omitempty"`
}

ContactLinkClick names the link behind an TimelineEmailClicked event: where it went, the anchor text it was minted from, and the UTM parameters the destination carried. Every link in an email is tracked on its own, so each link clicked is its own event. Clicks recorded before per-link attribution have no link.

type ContactNextAction added in v0.3.0

type ContactNextAction struct {
	// StepID is nil while a branch condition is still undecided, in which
	// case StepLabel says the step depends on the contact's response.
	StepID    *string `json:"step_id,omitempty"`
	StepLabel string  `json:"step_label"`
	Kind      string  `json:"kind,omitempty"`
	Subject   string  `json:"subject,omitempty"`
	// State is one of the NextAction* constants.
	State string `json:"state"`
	// ScheduledAt is set only when due; NotBefore is the earliest the hard
	// constraints allow; Constraint names the gate in user-facing words.
	ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
	NotBefore   *time.Time `json:"not_before,omitempty"`
	Constraint  string     `json:"constraint,omitempty"`
}

ContactNextAction is what happens next to a contact in a campaign. It is derived on read by the scheduler through the same constraints a real send goes through; nothing per contact is stored.

type ContactNote

type ContactNote struct {
	ID             string    `json:"id"`
	ContactID      string    `json:"contact_id"`
	OrganizationID string    `json:"organization_id"`
	UserID         string    `json:"user_id"`
	Content        string    `json:"content"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
	// User is the note's author, joined in on read.
	User *User `json:"user,omitempty"`
}

ContactNote is a free-text CRM note on a contact.

type ContactPage

type ContactPage struct {
	Page[Contact]

	// Counts are organization-wide facet totals, present on the first page only.
	Counts *ContactsCounts `json:"counts,omitempty"`
	// LeadCounts are per-status totals for a single-campaign search, present on
	// the first page only.
	LeadCounts *CampaignLeadCounts `json:"lead_counts,omitempty"`
}

ContactPage is one page of contact search results. Beyond the page itself it carries the facet counts the API returns on the first page.

type ContactPageHit added in v0.3.0

type ContactPageHit struct {
	ID             string    `json:"id"`
	VisitorID      string    `json:"visitor_id"`
	SessionKey     string    `json:"session_key"`
	OccurredAt     time.Time `json:"occurred_at"`
	URL            string    `json:"url"`
	Path           string    `json:"path"`
	Title          string    `json:"title"`
	Referrer       string    `json:"referrer"`
	ReferrerDomain string    `json:"referrer_domain"`
	Landing        bool      `json:"landing"`
	UTMSource      string    `json:"utm_source"`
	UTMMedium      string    `json:"utm_medium"`
	UTMCampaign    string    `json:"utm_campaign"`
	UTMTerm        string    `json:"utm_term"`
	UTMContent     string    `json:"utm_content"`
	DeviceType     string    `json:"device_type"`
	OS             string    `json:"os"`
	Browser        string    `json:"browser"`
	BrowserVersion string    `json:"browser_version"`
	DeviceBrand    string    `json:"device_brand"`
	Language       string    `json:"language"`
	Timezone       string    `json:"timezone"`
	ScreenWidth    int       `json:"screen_width"`
	ScreenHeight   int       `json:"screen_height"`
	CountryCode    string    `json:"country_code"`
	Region         string    `json:"region"`
	City           string    `json:"city"`
}

ContactPageHit is one page view from the website tracking snippet, as carried by a TimelinePageHit event. Landing marks the first view of a session; the device, language, screen and location fields are empty or zero when unknown.

type ContactSearchParams

type ContactSearchParams struct {
	ListOptions
	// Category is a convenience filter for a single category id.
	Category string `json:"-"`

	// Query is a free-text search across the core contact fields.
	Query              string               `json:"query,omitempty"`
	CustomFieldFilters []ContactFieldFilter `json:"custom_field_filters,omitempty"`
	// CampaignIDs matches contacts enrolled in every listed campaign.
	CampaignIDs []string `json:"campaign_ids,omitempty"`
	// LeadStatus narrows to one derived lead status (a LeadStatus* constant)
	// inside the single campaign in CampaignIDs. Any other number of campaigns
	// is rejected with code "lead_filter_requires_campaign"; an unknown value
	// with "invalid_lead_status".
	LeadStatus string `json:"lead_status,omitempty"`
	// Engagement narrows by engagement inside that same single campaign (a
	// LeadEngagement* constant), ANDed with LeadStatus. It has the same
	// single-campaign requirement; an unknown value is rejected with
	// "invalid_engagement".
	Engagement string `json:"engagement,omitempty"`
	// CategoryIDs matches contacts carrying every listed category.
	CategoryIDs []string `json:"category_ids,omitempty"`
	// SegmentIDs matches contacts that are members of every listed segment
	// (conditions plus manual overrides). A malformed id is a 400; an unknown
	// segment matches nothing.
	SegmentIDs   []string `json:"segment_ids,omitempty"`
	MinCampaigns *int     `json:"min_campaigns,omitempty"`
	MaxCampaigns *int     `json:"max_campaigns,omitempty"`
	Subscribed   *bool    `json:"subscribed,omitempty"`
	// VerificationStatus filters by verdict: one of the VerifyStatus*
	// constants.
	VerificationStatus string     `json:"verification_status,omitempty"`
	CreatedAfter       *time.Time `json:"created_after,omitempty"`
	CreatedBefore      *time.Time `json:"created_before,omitempty"`
	UpdatedAfter       *time.Time `json:"updated_after,omitempty"`
	UpdatedBefore      *time.Time `json:"updated_before,omitempty"`
	// SortBy names the column to order by, for example "first_name" or
	// "campaign_count".
	SortBy string `json:"sort_by,omitempty"`
	// Reverse switches the sort to descending.
	Reverse bool `json:"reverse,omitempty"`
}

ContactSearchParams filters and paginates a contact search. Cursor, Limit and Category travel in the query string; every other field is sent in the body.

type ContactSegmentMembership added in v0.3.0

type ContactSegmentMembership struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Color string `json:"color"`
	// Mode is "include" or "exclude" when the contact carries a manual
	// override, and empty when the conditions alone decide.
	Mode string `json:"mode,omitempty"`
	// Member reports whether the contact is currently in the segment.
	Member bool `json:"member"`
}

ContactSegmentMembership is one segment of the organization seen from a contact: whether the contact is in it right now and any manual override.

type ContactSentEmail

type ContactSentEmail struct {
	TaskID    string    `json:"task_id"`
	Status    string    `json:"status"`
	MessageID string    `json:"message_id"`
	Subject   string    `json:"subject"`
	SentAt    time.Time `json:"sent_at"`

	EmailAccountID    *string `json:"email_account_id,omitempty"`
	EmailAccountEmail *string `json:"email_account_email,omitempty"`
	EmailAccountName  *string `json:"email_account_name,omitempty"`

	CampaignID   *string `json:"campaign_id,omitempty"`
	CampaignName *string `json:"campaign_name,omitempty"`
	StepID       *string `json:"step_id,omitempty"`
	StepName     *string `json:"step_name,omitempty"`

	OpenedAt  *time.Time `json:"opened_at,omitempty"`
	ClickedAt *time.Time `json:"clicked_at,omitempty"`
	RepliedAt *time.Time `json:"replied_at,omitempty"`
	BouncedAt *time.Time `json:"bounced_at,omitempty"`
}

ContactSentEmail is one row in the list of emails sent to a contact.

type ContactService

type ContactService service

ContactService manages contacts (leads): search and bulk editing, the hydrated contact 360 view, address verification, per-campaign state, CRM notes and activities, import and export, and AI-assisted research.

func (*ContactService) Activities

func (s *ContactService) Activities(ctx context.Context, id string, params *ListOptions, opts ...RequestOption) (*Page[ContactActivity], error)

Activities returns a page of the contact's recorded CRM activities.

func (*ContactService) AddNote

func (s *ContactService) AddNote(ctx context.Context, id, content string, opts ...RequestOption) (*ContactNote, *Response, error)

AddNote appends a CRM note to the contact.

func (*ContactService) BatchResearch

func (s *ContactService) BatchResearch(ctx context.Context, contactIDs []string, objective string, opts ...RequestOption) (int, *Response, error)

BatchResearch queues AI research for many contacts and returns how many runs were enqueued. They drain in the background; poll ContactService.ListResearch or subscribe to the gateway for progress.

func (*ContactService) BulkDelete

func (s *ContactService) BulkDelete(ctx context.Context, ids []string, opts ...RequestOption) (*Response, error)

BulkDelete permanently removes the given contacts, at most 1,000 per request.

func (*ContactService) BulkUpdate

func (s *ContactService) BulkUpdate(ctx context.Context, params *ContactBulkUpdateParams, opts ...RequestOption) ([]Contact, *Response, error)

BulkUpdate edits many contacts at once and returns the updated records. It takes at most 1,000 contacts per request and never creates contacts, so it never raises contact.created.

func (*ContactService) CampaignStates added in v0.3.0

func (s *ContactService) CampaignStates(ctx context.Context, id string, opts ...RequestOption) ([]ContactCampaignState, *Response, error)

CampaignStates returns, for every campaign the contact is a lead of, the flow with the contact's progress on each step, the derived lead status, and the scheduler's next action.

func (*ContactService) Create

func (s *ContactService) Create(ctx context.Context, contacts []ContactInput, opts ...RequestOption) ([]Contact, *Response, error)

Create adds contacts to the organization and returns the resulting records, index-aligned with the input. An address that already exists is enriched rather than duplicated (see ContactInput). New addresses are queued for verification right away.

A contact.created webhook (and any automation it triggers) fires for each genuinely new row, but only when the request carries 100 contacts or fewer: a larger batch is treated as a bulk arrival, like a file import, and stays silent so one call cannot flood the organization's automations.

func (*ContactService) CustomFields

func (s *ContactService) CustomFields(ctx context.Context, opts ...RequestOption) ([]string, *Response, error)

CustomFields returns the organization's distinct contact custom-field keys, most common first, capped at 200. Use it to populate a merge-tag picker.

func (*ContactService) Deals

func (s *ContactService) Deals(ctx context.Context, id string, opts ...RequestOption) ([]Deal, *Response, error)

Deals returns the CRM deals attached to the contact.

func (*ContactService) Delete

func (s *ContactService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete permanently removes a contact.

func (*ContactService) DeleteNote

func (s *ContactService) DeleteNote(ctx context.Context, id, noteID string, opts ...RequestOption) (*Response, error)

DeleteNote removes a CRM note.

func (*ContactService) Emails

func (s *ContactService) Emails(ctx context.Context, id string, opts ...RequestOption) ([]ContactSentEmail, *Response, error)

Emails returns the emails sent to a contact.

func (*ContactService) Export

func (s *ContactService) Export(ctx context.Context, params *ContactExportParams, w io.Writer, opts ...RequestOption) (*Response, error)

Export streams the organization's contacts to w in the requested format. The response's X-Total-Rows header carries the row count.

func (*ContactService) Get

Get retrieves the hydrated contact 360 view.

func (*ContactService) ImportCommit

func (s *ContactService) ImportCommit(ctx context.Context, file *FileUpload, params *ContactImportParams, opts ...RequestOption) (*ContactImportResult, *Response, error)

ImportCommit imports the file using the given mapping and options. Pass the same file that was used for ContactService.ImportPreview. Imports are capped at 50,000 rows, and new addresses are queued for verification right away.

An import is a bulk arrival: it never raises contact.created, however few rows it has, so one upload cannot flood the organization's automations and webhooks.

func (*ContactService) ImportPreview

func (s *ContactService) ImportPreview(ctx context.Context, file *FileUpload, opts ...RequestOption) (*ContactImportPreview, *Response, error)

ImportPreview parses an uploaded CSV or XLSX and returns the detected columns and a suggested mapping, without writing anything.

func (*ContactService) ListResearch

func (s *ContactService) ListResearch(ctx context.Context, id string, limit int, opts ...RequestOption) ([]ResearchRun, *Response, error)

ListResearch returns the contact's most recent research runs, newest first. A limit of 0 uses the server default of 20; the cap is 100.

func (*ContactService) ListTimeline added in v0.3.0

func (s *ContactService) ListTimeline(ctx context.Context, id string, params *TimelineParams, opts ...RequestOption) (*Page[TimelineEvent], error)

ListTimeline returns a page of the contact's merged activity feed: sends, opens, clicks (one per link), replies, bounces, deliverability and suppression events, notes, meetings, lifecycle events and page views. Pages resume at the exact position of the last event, so events sharing a timestamp are never skipped or repeated. Pagination.Total is always nil: the feed is merged from several tables and never counted.

func (*ContactService) Lookup

func (s *ContactService) Lookup(ctx context.Context, email string, opts ...RequestOption) (*ContactDetail, *Response, error)

Lookup resolves an email address to a contact. A "Display Name <addr>" form is accepted and reduced to the bare address. The contact is nil when no contact in the organization owns that address.

func (*ContactService) Notes

func (s *ContactService) Notes(ctx context.Context, id string, params *ListOptions, opts ...RequestOption) (*Page[ContactNote], error)

Notes returns a page of the contact's CRM notes.

func (*ContactService) RequestVerification added in v0.3.0

RequestVerification queues a fresh check of the selected contacts or records a manual verdict on them, depending on ContactVerificationParams.Action. Verification runs in the background: poll the contacts or subscribe to the gateway to see verdicts land.

func (*ContactService) Research

func (s *ContactService) Research(ctx context.Context, id, objective string, opts ...RequestOption) (*ResearchRun, *Response, error)

Research runs AI research against a single contact, in the request. It spends AI credits.

func (*ContactService) Search

func (s *ContactService) Search(ctx context.Context, params *ContactSearchParams, opts ...RequestOption) (*ContactPage, error)

Search returns a page of contacts matching the filters.

func (*ContactService) Segments added in v0.3.0

Segments returns every segment in the organization with whether the contact is currently a member and any manual override on it. Membership is evaluated live, so the answer reflects the contact as it is now.

func (*ContactService) Timeline

func (s *ContactService) Timeline(ctx context.Context, id string, opts ...RequestOption) ([]TimelineEvent, *Response, error)

Timeline returns the first page of the contact's merged activity feed, newest first, using the server default of 50 events. Use ContactService.ListTimeline to page through the whole feed.

func (*ContactService) Update

func (s *ContactService) Update(ctx context.Context, id string, params *ContactUpdateParams, opts ...RequestOption) (*Contact, *Response, error)

Update modifies a single contact.

func (*ContactService) UpdateNote

func (s *ContactService) UpdateNote(ctx context.Context, id, noteID, content string, opts ...RequestOption) (*ContactNote, *Response, error)

UpdateNote rewrites a CRM note.

func (*ContactService) VerificationOverview added in v0.3.0

func (s *ContactService) VerificationOverview(ctx context.Context, opts ...RequestOption) (*ContactVerificationOverview, *Response, error)

VerificationOverview reports which verifier checks the workspace's addresses, its remaining credits, and the contacts by verdict.

type ContactSuppression

type ContactSuppression struct {
	// ID is the suppression-list entry; DELETE /suppressions/:id lifts it.
	ID string `json:"id"`
	// Kind is "email" when the contact's own address is on the list, or
	// "domain" when its whole domain is. Value is the matching entry.
	Kind   string `json:"kind"`
	Value  string `json:"value"`
	Reason string `json:"reason"`
	// Source is "bounce", "complaint", "unsubscribe", "manual" or "import".
	Source    string     `json:"source"`
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

ContactSuppression records why a contact's address is suppressed. It is nil when the contact is deliverable.

type ContactUpdateParams

type ContactUpdateParams struct {
	FirstName    *string           `json:"first_name,omitempty"`
	LastName     *string           `json:"last_name,omitempty"`
	Company      *string           `json:"company,omitempty"`
	Phone        *string           `json:"phone,omitempty"`
	CustomFields map[string]string `json:"custom_fields,omitempty"`
	Subscribed   *bool             `json:"subscribed,omitempty"`

	Campaigns        []string `json:"campaigns,omitempty"`
	Categories       []string `json:"categories,omitempty"`
	AddCategories    []string `json:"add_categories,omitempty"`
	RemoveCategories []string `json:"remove_categories,omitempty"`
}

ContactUpdateParams updates a single contact. Nil fields are unchanged.

Categories replaces the contact's categories wholesale, while AddCategories and RemoveCategories adjust them incrementally. Use one form or the other.

type ContactUsage

type ContactUsage struct {
	Total      int64 `json:"total"`
	Subscribed int64 `json:"subscribed"`
	AddedToday int64 `json:"added_today"`
}

ContactUsage counts contacts.

type ContactVerificationCounts added in v0.3.0

type ContactVerificationCounts struct {
	Valid   int `json:"valid"`
	Risky   int `json:"risky"`
	Invalid int `json:"invalid"`
	Unknown int `json:"unknown"`
	Pending int `json:"pending"`
}

ContactVerificationCounts is how many of the organization's contacts carry each verification status. Pending is the subset of Unknown nobody has checked yet.

type ContactVerificationDetail added in v0.3.0

type ContactVerificationDetail struct {
	// Status is one of the VerifyStatus* constants.
	Status     string `json:"status"`
	Confidence int    `json:"confidence"`
	// Reasons are sentences, strongest first.
	Reasons []string `json:"reasons"`
	// Decisive is true when real mail, rather than a check, decided the status.
	Decisive bool `json:"decisive"`
	// Evidence lists the observations the score came from, newest first.
	Evidence []ContactVerificationEvidence `json:"evidence"`
}

ContactVerificationDetail is the "why" behind a contact's verdict, returned on the contact 360 view.

type ContactVerificationEvidence added in v0.3.0

type ContactVerificationEvidence struct {
	// Kind is one of the VerificationEvidence* constants.
	Kind       string    `json:"kind"`
	Detail     string    `json:"detail,omitempty"`
	ObservedAt time.Time `json:"observed_at"`
}

ContactVerificationEvidence is one observed fact about the mailbox that fed the verification confidence.

type ContactVerificationOverview added in v0.3.0

type ContactVerificationOverview struct {
	// Provider is the verifier in use: [VerificationProviderBuiltin] or
	// [VerificationProviderMillionVerifier].
	Provider string `json:"provider"`
	// ConnectionID is the integration connection behind a paid provider.
	ConnectionID *string `json:"connection_id,omitempty"`
	// Credits is the paid provider's remaining balance, when it could be read.
	Credits *int `json:"credits,omitempty"`
	// ProviderError is set when a paid provider is connected but unusable (a
	// rejected key, no credits); the built-in check is in use meanwhile.
	ProviderError string `json:"provider_error,omitempty"`
	// BuiltinReady says whether the built-in mailbox probe can reach mail
	// servers from this instance. Off, it still checks syntax, MX and
	// disposable domains.
	BuiltinReady bool                      `json:"builtin_ready"`
	Counts       ContactVerificationCounts `json:"counts"`
}

ContactVerificationOverview says who checks the workspace's addresses and how its contacts split by verdict.

type ContactVerificationParams added in v0.3.0

type ContactVerificationParams struct {
	// Action is one of the VerificationAction* constants. Anything else is
	// rejected with code "invalid_action".
	Action string `json:"action"`
	// Contacts are contact ids, at most 1,000 per request
	// ("too_many_contacts").
	Contacts []string `json:"contacts,omitempty"`
	// CampaignID selects every lead of the campaign that verification refused
	// (the [LeadStatusUndeliverable] ones), instead of or as well as Contacts.
	CampaignID string `json:"campaign_id,omitempty"`
}

ContactVerificationParams selects contacts for ContactService.RequestVerification. Contacts and CampaignID combine; at least one contact must be selected or the request is rejected with code "no_contacts".

type ContactVerificationResult added in v0.3.0

type ContactVerificationResult struct {
	Affected int    `json:"affected"`
	Action   string `json:"action"`
	// Queued is true for [VerificationActionVerify]: the check runs in the
	// background rather than in the request.
	Queued bool `json:"queued"`
}

ContactVerificationResult reports how many contacts a verification action touched.

type ContactsCounts

type ContactsCounts struct {
	Total        int                       `json:"total"`
	Subscribed   int                       `json:"subscribed"`
	Unsubscribed int                       `json:"unsubscribed"`
	InCampaign   int                       `json:"in_campaign"`
	NotContacted int                       `json:"not_contacted"`
	Categories   []ContactCategoryCount    `json:"categories"`
	Verification ContactVerificationCounts `json:"verification"`
}

ContactsCounts are organization-wide contact facet totals, returned on the first page of a search for the browse sidebar. They are independent of the search's own filters.

type CreditBalance

type CreditBalance struct {
	// Unlimited is true on a deployment with no billing provider: AI is not
	// metered there, every numeric field is zero and Packs is empty. Check it
	// before rendering a balance as "0 left".
	Unlimited bool `json:"unlimited"`
	// Balance is the total spendable amount across both pools.
	Balance          int `json:"balance"`
	MonthlyBalance   int `json:"monthly_balance"`
	PurchasedBalance int `json:"purchased_balance"`
	// MonthlyAllowance is what the plan grants each month.
	MonthlyAllowance int `json:"monthly_allowance"`
	TotalPurchased   int `json:"total_purchased"`
	// MonthlyResetAt is when the monthly pool refills; NextResetAt is the end
	// of the billing period.
	MonthlyResetAt *time.Time `json:"monthly_reset_at,omitempty"`
	NextResetAt    *time.Time `json:"next_reset_at,omitempty"`
	// Packs are the top-up bundles available for purchase.
	Packs []CreditPack `json:"packs,omitempty"`
}

CreditBalance is the workspace's AI credit position across both pools: the monthly allowance that resets, and purchased credits that do not.

type CreditContext

type CreditContext struct {
	Detail     string `json:"detail,omitempty"`
	ThreadID   string `json:"thread_id,omitempty"`
	CampaignID string `json:"campaign_id,omitempty"`
	ContactID  string `json:"contact_id,omitempty"`
}

CreditContext names what a charge was actually for.

type CreditPack

type CreditPack struct {
	Key     string `json:"key"`
	Credits int    `json:"credits"`
	// PriceCents is the pack's price in the smallest currency unit.
	PriceCents int    `json:"price_cents,omitempty"`
	Currency   string `json:"currency,omitempty"`
	Label      string `json:"label,omitempty"`
}

CreditPack is a purchasable top-up bundle.

type CreditSettings

type CreditSettings struct {
	OrgID string `json:"org_id"`

	// SpendLimit* cap workspace-wide spend per window. A nil value means no
	// limit.
	SpendLimitDaily   *int `json:"spend_limit_daily"`
	SpendLimitWeekly  *int `json:"spend_limit_weekly"`
	SpendLimitMonthly *int `json:"spend_limit_monthly"`

	// MemberLimit* cap what one member can spend per window. Scheduled and
	// system work is not counted against anyone.
	MemberLimitDaily   *int `json:"member_limit_daily"`
	MemberLimitWeekly  *int `json:"member_limit_weekly"`
	MemberLimitMonthly *int `json:"member_limit_monthly"`

	// LowBalanceThreshold is the balance at which an alert fires.
	LowBalanceThreshold  int        `json:"low_balance_threshold"`
	LowBalanceNotifiedAt *time.Time `json:"low_balance_notified_at,omitempty"`

	// AutoTopup buys AutoTopupPack whenever the balance falls below
	// AutoTopupThreshold, at most AutoTopupMaxPerMonth times a month.
	AutoTopupEnabled     bool   `json:"auto_topup_enabled"`
	AutoTopupPack        string `json:"auto_topup_pack"`
	AutoTopupThreshold   int    `json:"auto_topup_threshold"`
	AutoTopupMaxPerMonth int    `json:"auto_topup_max_per_month"`

	CreatedAt time.Time `json:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

CreditSettings are the workspace's AI spend controls.

type CreditSettingsParams

type CreditSettingsParams struct {
	SpendLimitDaily   *int `json:"spend_limit_daily,omitempty"`
	SpendLimitWeekly  *int `json:"spend_limit_weekly,omitempty"`
	SpendLimitMonthly *int `json:"spend_limit_monthly,omitempty"`

	MemberLimitDaily   *int `json:"member_limit_daily,omitempty"`
	MemberLimitWeekly  *int `json:"member_limit_weekly,omitempty"`
	MemberLimitMonthly *int `json:"member_limit_monthly,omitempty"`

	LowBalanceThreshold  int    `json:"low_balance_threshold,omitempty"`
	AutoTopupEnabled     bool   `json:"auto_topup_enabled,omitempty"`
	AutoTopupPack        string `json:"auto_topup_pack,omitempty"`
	AutoTopupThreshold   int    `json:"auto_topup_threshold,omitempty"`
	AutoTopupMaxPerMonth int    `json:"auto_topup_max_per_month,omitempty"`
}

CreditSettingsParams updates the spend controls. An omitted limit disables that limit.

type CreditTransaction

type CreditTransaction struct {
	ID     string `json:"id"`
	OrgID  string `json:"org_id"`
	Amount int    `json:"amount"`
	// Reason names the feature that spent, for example "reply_draft".
	Reason     string `json:"reason"`
	ModelUsed  string `json:"model_used,omitempty"`
	TokensUsed int    `json:"tokens_used"`
	// BalanceAfter is the resulting total, captured atomically with the
	// charge.
	BalanceAfter int `json:"balance_after"`
	// PurchasedDelta and PurchasedBalanceAfter track the purchased pool
	// separately, so the log reconstructs both.
	PurchasedDelta        int     `json:"purchased_delta"`
	PurchasedBalanceAfter int     `json:"purchased_balance_after"`
	IdempotencyKey        *string `json:"idempotency_key,omitempty"`
	// ActorUserID is the member who triggered the charge; it is nil for
	// scheduled or system work.
	ActorUserID *string       `json:"actor_user_id,omitempty"`
	Context     CreditContext `json:"context"`
	CreatedAt   time.Time     `json:"created_at"`
}

CreditTransaction is one row of the append-only credit ledger. Amount is negative for spend and positive for grants and purchases.

type CreditUsage

type CreditUsage struct {
	SpentToday int `json:"spent_today"`
	SpentWeek  int `json:"spent_week"`
	SpentMonth int `json:"spent_month"`

	// LimitDaily, LimitWeekly and LimitMonthly are the configured spend caps.
	// A nil value means no cap for that window.
	LimitDaily   *int `json:"limit_daily"`
	LimitWeekly  *int `json:"limit_weekly"`
	LimitMonthly *int `json:"limit_monthly"`

	Series   []CreditUsagePoint  `json:"series"`
	ByReason []CreditUsageBucket `json:"by_reason"`
	ByModel  []CreditUsageBucket `json:"by_model"`
}

CreditUsage is AI spend over a window, with the daily series and breakdowns by feature and model.

type CreditUsageBucket

type CreditUsageBucket struct {
	Key     string `json:"key"`
	Credits int    `json:"credits"`
	Tokens  int    `json:"tokens"`
	// Count is how many charges landed in this bucket.
	Count int `json:"count"`
}

CreditUsageBucket is one slice of a usage breakdown.

type CreditUsagePoint

type CreditUsagePoint struct {
	// Date is a UTC calendar day formatted YYYY-MM-DD.
	Date    string `json:"date"`
	Credits int    `json:"credits"`
	Tokens  int    `json:"tokens"`
}

CreditUsagePoint is one day of AI spend.

type DailyPlan added in v0.3.0

type DailyPlan struct {
	EmailAccountID string `json:"email_account_id"`
	// PlanDate is the local calendar date in Timezone, as YYYY-MM-DD.
	PlanDate string `json:"plan_date"`
	Timezone string `json:"timezone"`

	// IsWorkingDay is false on a day the profile does not send at all.
	IsWorkingDay bool `json:"is_working_day"`

	// DailyLimit and HourlyLimit are the day's rolled cold-send target and
	// hourly ceiling.
	DailyLimit  int `json:"daily_limit"`
	HourlyLimit int `json:"hourly_limit"`
	// WorkStartMinute and WorkEndMinute are the rolled workday as minutes
	// since local midnight.
	WorkStartMinute int `json:"work_start_minute"`
	WorkEndMinute   int `json:"work_end_minute"`
	// LunchStartMinute and LunchEndMinute bound the rolled break; nil when
	// the day carries none.
	LunchStartMinute *int `json:"lunch_start_minute"`
	LunchEndMinute   *int `json:"lunch_end_minute"`
	GapMinSeconds    int  `json:"gap_min_seconds"`
	GapMaxSeconds    int  `json:"gap_max_seconds"`

	CreatedAt time.Time `json:"created_at"`

	// SentToday is completed cold sends from this mailbox on this local date.
	SentToday int `json:"sent_today"`
	// RemainingToday is what the plan still allows, floored at zero.
	RemainingToday int `json:"remaining_today"`
	// Behavior is the profile the plan was rolled from.
	Behavior SendingBehavior `json:"behavior"`
}

DailyPlan is the workday a mailbox actually rolled for the current local date, plus how much of it is already spent. It is rolled once per local day and never updated, so every scheduling pass reads the same numbers. This is the read that answers "why is nothing sending right now".

func (*DailyPlan) HasLunch added in v0.3.0

func (p *DailyPlan) HasLunch() bool

HasLunch reports whether this day carries a break.

type DailyStat

type DailyStat struct {
	// Date is a calendar day formatted YYYY-MM-DD.
	Date    string `json:"date"`
	Sent    int64  `json:"sent"`
	Opens   int64  `json:"opens"`
	Clicks  int64  `json:"clicks"`
	Replies int64  `json:"replies"`
}

DailyStat is one day of engagement on a trend line.

type DangerZoneStatus

type DangerZoneStatus struct {
	// ResourceType is "organization" or "user".
	ResourceType string `json:"resource_type"`
	ResourceID   string `json:"resource_id"`
	ResourceName string `json:"resource_name"`
	// ConfirmationHint is what the user must type to confirm: the workspace
	// name, or their own email address.
	ConfirmationHint string `json:"confirmation_hint"`
	// GraceDays is how long the delete is delayed and remains cancelable.
	GraceDays int `json:"grace_days"`
	// PendingDeletion is set only while a delete is scheduled.
	PendingDeletion *ScheduledDeletion `json:"pending_deletion,omitempty"`
}

DangerZoneStatus describes a workspace's or account's deletion state, plus the confirmation phrase a client should require before scheduling one.

type DashboardAnalytics

type DashboardAnalytics struct {
	// Period is [Period7Days], [Period30Days] or [Period90Days].
	Period         string              `json:"period"`
	OverallStats   OverallStats        `json:"overall_stats"`
	RecentActivity []ActivityEvent     `json:"recent_activity"`
	TopCampaigns   []CampaignSummary   `json:"top_campaigns"`
	AccountHealth  AccountHealthTotals `json:"account_health"`
	DailyTrend     []DailyStat         `json:"daily_trend"`
}

DashboardAnalytics is the organization-wide engagement summary.

type DateRange

type DateRange struct {
	From time.Time `json:"from"`
	To   time.Time `json:"to"`
}

DateRange is the window an analytics response covers.

type DeadLetter

type DeadLetter struct {
	ID     string `json:"id"`
	TaskID string `json:"task_id"`
	// TaskType is what the task was doing, for example "campaign_email".
	TaskType string `json:"task_type"`
	// Payload is the original task body, kept so a replay is faithful.
	Payload     json.RawMessage `json:"payload,omitempty"`
	LastError   string          `json:"last_error,omitempty"`
	Attempts    int             `json:"attempts"`
	MaxAttempts int             `json:"max_attempts"`
	// Status is the queue state, for example "pending" or "replayed".
	Status      string     `json:"status"`
	NextRetryAt *time.Time `json:"next_retry_at,omitempty"`
	ReplayedAt  *time.Time `json:"replayed_at,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	UpdatedAt   time.Time  `json:"updated_at"`
}

DeadLetter is one parked task.

type DeadLetterListParams

type DeadLetterListParams struct {
	// Status filters by queue state, for example "pending".
	Status string
	// Limit caps the rows returned, from 1 to 200. Zero uses the server
	// default of 100.
	Limit int
}

DeadLetterListParams filters the dead-letter queue.

type Deal

type Deal struct {
	ID             string  `json:"id"`
	OrganizationID string  `json:"organization_id"`
	PipelineID     string  `json:"pipeline_id"`
	StageID        string  `json:"stage_id"`
	ContactID      *string `json:"contact_id,omitempty"`
	Name           string  `json:"name"`
	// Value is the deal's amount in Currency.
	Value    *float64 `json:"value,omitempty"`
	Currency string   `json:"currency"`
	// Status is [DealStatusOpen], [DealStatusWon] or [DealStatusLost].
	Status            string     `json:"status"`
	ExpectedCloseDate *time.Time `json:"expected_close_date,omitempty"`
	WonAt             *time.Time `json:"won_at,omitempty"`
	LostAt            *time.Time `json:"lost_at,omitempty"`
	LostReason        *string    `json:"lost_reason,omitempty"`
	AssignedTo        *string    `json:"assigned_to,omitempty"`

	// CampaignID and SourceMailboxID attribute the deal to the outreach that
	// produced it. They are a best guess and are editable.
	CampaignID      *string `json:"campaign_id,omitempty"`
	SourceMailboxID *string `json:"source_mailbox_id,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`

	// Contact, Stage and CampaignName are joined in by the list and search
	// endpoints, not by single-deal reads.
	Contact      *Contact       `json:"contact,omitempty"`
	Stage        *PipelineStage `json:"stage,omitempty"`
	CampaignName *string        `json:"campaign_name,omitempty"`
}

Deal is a sales opportunity moving through a pipeline.

type DealCreateParams

type DealCreateParams struct {
	PipelineID        string     `json:"pipeline_id"`
	StageID           string     `json:"stage_id"`
	ContactID         *string    `json:"contact_id,omitempty"`
	Name              string     `json:"name"`
	Value             *float64   `json:"value,omitempty"`
	Currency          string     `json:"currency,omitempty"`
	ExpectedCloseDate *time.Time `json:"expected_close_date,omitempty"`
	AssignedTo        *string    `json:"assigned_to,omitempty"`
	CampaignID        *string    `json:"campaign_id,omitempty"`
	SourceMailboxID   *string    `json:"source_mailbox_id,omitempty"`
}

DealCreateParams creates a deal. PipelineID, StageID and Name are required.

type DealListParams added in v0.3.0

type DealListParams struct {
	ListOptions
	// PipelineID and StageID narrow to one pipeline or one of its columns.
	PipelineID string
	StageID    string
	// Status is [DealStatusOpen], [DealStatusWon] or [DealStatusLost].
	Status string
}

DealListParams filters and paginates the plain deal list. Every filter is optional and matches a single value; use DealSearchParams with CRMService.SearchDeals when you need several values per facet, a value range or an exact total.

type DealSearchParams

type DealSearchParams struct {
	ListOptions

	// Query matches the deal name.
	Query string `json:"query,omitempty"`
	// Statuses matches any of [DealStatusOpen], [DealStatusWon] or
	// [DealStatusLost].
	Statuses    []string `json:"statuses,omitempty"`
	PipelineIDs []string `json:"pipeline_ids,omitempty"`
	StageIDs    []string `json:"stage_ids,omitempty"`
	// AssignedTo matches any of the given owner user ids.
	AssignedTo  []string `json:"assigned_to,omitempty"`
	CampaignIDs []string `json:"campaign_ids,omitempty"`

	MinValue      *float64   `json:"min_value,omitempty"`
	MaxValue      *float64   `json:"max_value,omitempty"`
	CloseAfter    *time.Time `json:"close_after,omitempty"`
	CloseBefore   *time.Time `json:"close_before,omitempty"`
	CreatedAfter  *time.Time `json:"created_after,omitempty"`
	CreatedBefore *time.Time `json:"created_before,omitempty"`

	// SortBy is "created_at", "updated_at", "value", "expected_close_date" or
	// "name". Reverse switches to ascending; the default is descending.
	SortBy  string `json:"sort_by,omitempty"`
	Reverse bool   `json:"reverse,omitempty"`
}

DealSearchParams is the faceted filter shared by CRMService.SearchDeals and CRMService.DealsSummary. Every facet is optional; an empty body matches every deal in the organization. Slice facets match any of their values.

type DealStageSummary

type DealStageSummary struct {
	StageID string  `json:"stage_id"`
	Count   int64   `json:"count"`
	Value   float64 `json:"value"`
}

DealStageSummary is one pipeline column's count and open value.

type DealUpdateParams

type DealUpdateParams struct {
	StageID           *string    `json:"stage_id,omitempty"`
	ContactID         *string    `json:"contact_id,omitempty"`
	Name              *string    `json:"name,omitempty"`
	Value             *float64   `json:"value,omitempty"`
	Currency          *string    `json:"currency,omitempty"`
	Status            *string    `json:"status,omitempty"`
	ExpectedCloseDate *time.Time `json:"expected_close_date,omitempty"`
	LostReason        *string    `json:"lost_reason,omitempty"`
	AssignedTo        *string    `json:"assigned_to,omitempty"`
}

DealUpdateParams updates a deal. Nil fields are left unchanged. Setting Status to DealStatusWon or DealStatusLost closes it.

type DealsSummary

type DealsSummary struct {
	Total     int64              `json:"total"`
	OpenCount int64              `json:"open_count"`
	OpenValue float64            `json:"open_value"`
	WonCount  int64              `json:"won_count"`
	WonValue  float64            `json:"won_value"`
	LostCount int64              `json:"lost_count"`
	LostValue float64            `json:"lost_value"`
	Currency  string             `json:"currency"`
	Stages    []DealStageSummary `json:"stages"`
	// MixedCurrency is true when the matching deals span several currencies,
	// in which case the value sums are not directly comparable.
	MixedCurrency bool `json:"mixed_currency"`
}

DealsSummary aggregates every deal matching a DealSearchParams, so a header total or a kanban column sum is exact rather than a page reduce.

type DeliverabilityBreakdown

type DeliverabilityBreakdown struct {
	EmailAccountID string `json:"email_account_id,omitempty"`
	Email          string `json:"email,omitempty"`
	CampaignID     string `json:"campaign_id,omitempty"`
	Name           string `json:"name,omitempty"`

	Sent          int64   `json:"sent"`
	Bounces       int64   `json:"bounces"`
	Complaints    int64   `json:"complaints"`
	BounceRate    float64 `json:"bounce_rate"`
	ComplaintRate float64 `json:"complaint_rate"`
	// Band is one of the Band* constants.
	Band string `json:"band"`
}

DeliverabilityBreakdown is one mailbox's or campaign's contribution to deliverability. EmailAccountID and Email are set on the mailbox breakdown; CampaignID and Name on the campaign breakdown.

type DeliverabilityDashboard

type DeliverabilityDashboard struct {
	From time.Time `json:"from"`
	To   time.Time `json:"to"`

	EventsTotal          int64 `json:"events_total"`
	BounceCount          int64 `json:"bounce_count"`
	ComplaintCount       int64 `json:"complaint_count"`
	UnsubscribeCount     int64 `json:"unsubscribe_count"`
	ReplyCount           int64 `json:"reply_count"`
	OpenCount            int64 `json:"open_count"`
	ClickCount           int64 `json:"click_count"`
	SuppressedRecipients int64 `json:"suppressed_recipients"`
	// DLQPending is how many send tasks are parked in the dead-letter queue.
	DLQPending int64 `json:"dlq_pending"`

	IntentPositive    int64 `json:"intent_positive"`
	IntentNegative    int64 `json:"intent_negative"`
	IntentOutOfOffice int64 `json:"intent_out_of_office"`
	IntentQuestion    int64 `json:"intent_question"`
	IntentNeutral     int64 `json:"intent_neutral"`

	EmailsSent    int64   `json:"emails_sent"`
	BounceRate    float64 `json:"bounce_rate"`
	ComplaintRate float64 `json:"complaint_rate"`
	OpenRate      float64 `json:"open_rate"`
	ClickRate     float64 `json:"click_rate"`
	ReplyRate     float64 `json:"reply_rate"`

	// SpamPlacementRate and InboxPlacementRate come from seed-inbox testing;
	// PlacementSamples is how many seeds backed them. Both rates are omitted
	// (and decode as zero) when the window has no seed samples.
	SpamPlacementRate  float64 `json:"spam_placement_rate"`
	InboxPlacementRate float64 `json:"inbox_placement_rate"`
	PlacementSamples   int64   `json:"placement_samples"`

	// Band is the overall health verdict: one of the Band* constants. Score
	// folds the same bounce, complaint and spam-placement rates into a 0 to
	// 100 composite, higher being healthier.
	Band  string `json:"band"`
	Score int    `json:"score"`

	Timeseries []DeliverabilityDay       `json:"timeseries,omitempty"`
	ByMailbox  []DeliverabilityBreakdown `json:"by_mailbox,omitempty"`
	ByCampaign []DeliverabilityBreakdown `json:"by_campaign,omitempty"`
	// ByProvider breaks the seed placement results down per recipient
	// provider.
	ByProvider []ProviderPlacement `json:"by_provider,omitempty"`
	// WarmupPlacement is the continuous warmup-derived placement signal per
	// recipient domain.
	WarmupPlacement []WarmupDomainPlacement `json:"warmup_placement,omitempty"`
}

DeliverabilityDashboard is the organization's sending health over a window: bounce and complaint pressure, inbox placement, reply intent, and the mailboxes and campaigns driving it.

type DeliverabilityDashboardSettings

type DeliverabilityDashboardSettings struct {
	Enabled            bool `json:"enabled"`
	ShowSuppressionLog bool `json:"show_suppression_log"`
	ShowIntentSummary  bool `json:"show_intent_summary"`
	ShowDLQStats       bool `json:"show_dlq_stats"`
}

DeliverabilityDashboardSettings toggles the panels on the deliverability view.

type DeliverabilityDay

type DeliverabilityDay struct {
	Date         string `json:"date"`
	Sent         int64  `json:"sent"`
	Bounces      int64  `json:"bounces"`
	Complaints   int64  `json:"complaints"`
	Opens        int64  `json:"opens"`
	Clicks       int64  `json:"clicks"`
	Replies      int64  `json:"replies"`
	Unsubscribes int64  `json:"unsubscribes"`
}

DeliverabilityDay is one day on the deliverability trend line.

type DeliverabilityEventParams

type DeliverabilityEventParams struct {
	// EventType is one of the DeliverabilityEvent* constants.
	EventType      string `json:"event_type"`
	RecipientEmail string `json:"recipient_email"`
	// CampaignID, TaskID and ContactID attribute the event when known.
	CampaignID string `json:"campaign_id,omitempty"`
	TaskID     string `json:"task_id,omitempty"`
	ContactID  string `json:"contact_id,omitempty"`
	// Provider names the upstream that reported the event, for example "ses".
	Provider string `json:"provider,omitempty"`
	Reason   string `json:"reason,omitempty"`
	// IdempotencyKey deduplicates the event body-side, independently of the
	// Idempotency-Key header.
	IdempotencyKey string         `json:"idempotency_key,omitempty"`
	Metadata       map[string]any `json:"metadata,omitempty"`
}

DeliverabilityEventParams describes a single deliverability event.

type DeliverabilityService

type DeliverabilityService service

DeliverabilityService ingests deliverability events (bounces, complaints, deferrals) from an upstream mail pipeline, so a downstream processor such as an SES bounce handler can feed Warmbly's suppression list directly.

func (*DeliverabilityService) Ingest

Ingest records a deliverability event for the organization. The API accepts the event asynchronously and answers 202 with no body.

type DiscountPreview added in v0.3.0

type DiscountPreview struct {
	Valid  bool   `json:"valid"`
	Reason string `json:"reason,omitempty"`

	Code string `json:"code,omitempty"`
	// Type is one of the DiscountType* constants; exactly one of PercentOff,
	// AmountOff and TrialExtensionDays is set to match it.
	Type               string   `json:"type,omitempty"`
	PercentOff         *int     `json:"percent_off,omitempty"`
	AmountOff          *float64 `json:"amount_off,omitempty"`
	Currency           *string  `json:"currency,omitempty"`
	TrialExtensionDays *int     `json:"trial_extension_days,omitempty"`
	// Duration is one of the DiscountDuration* constants, with
	// DurationInMonths set when it repeats.
	Duration         string `json:"duration,omitempty"`
	DurationInMonths *int   `json:"duration_in_months,omitempty"`

	// OriginalAmount, DiscountedAmount and SavingsAmount are only computed
	// when a plan was named and the code is a money discount.
	OriginalAmount   *float64 `json:"original_amount,omitempty"`
	DiscountedAmount *float64 `json:"discounted_amount,omitempty"`
	SavingsAmount    *float64 `json:"savings_amount,omitempty"`
}

DiscountPreview is what a promotion code would do. Check Valid first: an unusable code answers 200 with Valid false and a Reason, rather than an error.

type DiscountRedemption added in v0.3.0

type DiscountRedemption struct {
	ID             string  `json:"id"`
	DiscountCodeID string  `json:"discount_code_id"`
	OrganizationID string  `json:"organization_id"`
	RedeemedBy     *string `json:"redeemed_by,omitempty"`
	SubscriptionID *string `json:"subscription_id,omitempty"`
	PlanID         *string `json:"plan_id,omitempty"`

	StripeCouponID          *string `json:"stripe_coupon_id,omitempty"`
	StripeCheckoutSessionID *string `json:"stripe_checkout_session_id,omitempty"`

	// Type is one of the DiscountType* constants.
	Type               string   `json:"type"`
	PercentOff         *int     `json:"percent_off,omitempty"`
	AmountOff          *float64 `json:"amount_off,omitempty"`
	Currency           *string  `json:"currency,omitempty"`
	TrialExtensionDays *int     `json:"trial_extension_days,omitempty"`

	// Status is one of the DiscountRedemption* constants.
	Status     string     `json:"status"`
	RedeemedAt time.Time  `json:"redeemed_at"`
	AppliedAt  *time.Time `json:"applied_at,omitempty"`

	// Code is the promotion's code, when the API joins it in.
	Code string `json:"code,omitempty"`
}

DiscountRedemption is one promotion code the workspace has redeemed.

type DomainAuthCheck

type DomainAuthCheck struct {
	Domain    string `json:"domain"`
	SPFFound  bool   `json:"spf_found"`
	SPFRecord string `json:"spf_record,omitempty"`
	// DKIMFound is advisory: selectors are not discoverable from DNS, so a
	// missing DKIM never fails the domain on its own.
	DKIMFound     bool     `json:"dkim_found"`
	DKIMSelectors []string `json:"dkim_selectors,omitempty"`
	DMARCFound    bool     `json:"dmarc_found"`
	// DMARCPolicy is the record's p= value, or its sp= value when the policy
	// is inherited from the organizational domain.
	DMARCPolicy string `json:"dmarc_policy,omitempty"`
	// DMARCDomain is where the DMARC record was actually found. It differs
	// from Domain when the policy is inherited.
	DMARCDomain string `json:"dmarc_domain,omitempty"`
	// DMARCInherited reports that the sending domain has no DMARC record of
	// its own and is covered by its organizational domain's policy, which is
	// how a dedicated sending subdomain normally works. SPF never inherits.
	DMARCInherited bool `json:"dmarc_inherited"`
	// Reserved marks a special-use domain (.test, .invalid, .localhost,
	// .example, .local) that cannot resolve by definition. It is recorded as
	// [AuthStateUnknown] rather than failing.
	Reserved   bool `json:"reserved"`
	AllAligned bool `json:"all_aligned"`
	// LookupError is true when an authoritative lookup failed transiently
	// (timeout, SERVFAIL, network) rather than the record being absent. Treat
	// the result as unknown, not as a misconfiguration.
	LookupError bool   `json:"lookup_error"`
	Summary     string `json:"summary"`
}

DomainAuthCheck is the live SPF/DKIM/DMARC lookup for a sending domain.

type DynamicClient

type DynamicClient struct {
	ClientID string `json:"client_id"`
	// ClientSecret is set only for a confidential registration.
	ClientSecret string `json:"client_secret,omitempty"`
	// ClientIDIssuedAt is a Unix timestamp.
	ClientIDIssuedAt int64 `json:"client_id_issued_at"`

	ClientName              string   `json:"client_name,omitempty"`
	RedirectURIs            []string `json:"redirect_uris"`
	GrantTypes              []string `json:"grant_types"`
	ResponseTypes           []string `json:"response_types"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method"`
	// Scope is what was actually granted, which is capped below what a
	// human-registered application can hold: a self-registered client can
	// never send mail or mint credentials.
	Scope string `json:"scope"`
}

DynamicClient is the RFC 7591 client-information response.

type DynamicClientParams

type DynamicClientParams struct {
	ClientName   string   `json:"client_name,omitempty"`
	RedirectURIs []string `json:"redirect_uris"`
	// GrantTypes and ResponseTypes default to the authorization-code flow.
	GrantTypes    []string `json:"grant_types,omitempty"`
	ResponseTypes []string `json:"response_types,omitempty"`
	// TokenEndpointAuthMethod of "none" registers a public client, which
	// authenticates with PKCE and holds no secret.
	TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
	// Scope is the space-delimited set being asked for.
	Scope     string `json:"scope,omitempty"`
	ClientURI string `json:"client_uri,omitempty"`
	LogoURI   string `json:"logo_uri,omitempty"`
}

DynamicClientParams self-registers a client at runtime, the RFC 7591 way that an MCP client uses to get credentials without a human registering an app first.

type EditParams

type EditParams struct {
	// Text is the passage to rewrite.
	Text string `json:"text"`
	// Instruction is what to change about it.
	Instruction string `json:"instruction"`
	// Context is the surrounding draft, which is fenced so quoted inbound mail
	// inside it cannot steer the rewrite.
	Context string `json:"context,omitempty"`
	Tone    string `json:"tone,omitempty"`
}

EditParams rewrites an existing passage.

type Email

type Email struct {
	ID             string  `json:"id"`
	UserID         string  `json:"user_id"`
	OrganizationID *string `json:"organization_id,omitempty"`
	// WorkerID identifies the worker currently handling this mailbox, when assigned.
	WorkerID *string `json:"worker_id"`
	Email    string  `json:"email"`

	Name           string `json:"name"`
	SignaturePlain string `json:"signature_plain"`
	SignatureHTML  string `json:"signature_html"`
	// SignatureSync mirrors the signature configured at the provider instead of
	// the one stored here.
	SignatureSync bool `json:"signature_sync"`
	// SignatureCode reports whether the HTML signature is edited as raw markup.
	SignatureCode bool `json:"signature_code"`

	// Provider is the mailbox backend: [ProviderGmail], [ProviderOutlook] or
	// [ProviderSMTPIMAP].
	Provider string `json:"provider"`
	// Status is the connection state: [MailboxStatusActive],
	// [MailboxStatusInactive] or [MailboxStatusRevoked].
	Status string `json:"status"`

	// LastSyncedAt is when the mailbox was last polled for inbound mail; nil
	// until the first sync has run. [EmailService.SyncStatus] has the detail.
	LastSyncedAt *time.Time `json:"last_synced_at"`
	// LastID is the provider-side sync watermark.
	LastID *int64 `json:"last_id"`

	// CampaignLimit caps how many campaign messages a day this mailbox sends
	// (0 to 5000; the default is 50). It is a ceiling only: the campaign's
	// daily limit, the warmup ramp, sending behavior and the workspace's daily
	// send limit all still apply and the smallest wins.
	CampaignLimit int `json:"campaign_limit"`
	// MinWaitTime is the minimum gap, in seconds, between two sends (default
	// 600). Jitter is added on top. While sending behavior is enabled the
	// behavior's gap range replaces it.
	MinWaitTime int `json:"min_wait_time"`
	// ReplyTo is the Reply-To address; empty uses the mailbox address.
	ReplyTo string `json:"reply_to"`

	// SaveToSent applies to SMTP/IMAP mailboxes only: after a send the worker
	// files a copy in the mailbox's Sent folder, since SMTP submission puts
	// nothing in the account by itself. Gmail and Outlook file their own copy
	// and ignore the flag. Warmup mail is never filed.
	SaveToSent bool `json:"save_to_sent"`

	// TrackingDomain is the custom open/click tracking subdomain, or "" for
	// the shared host. Only a verified domain is used at send time; see
	// [EmailService.GetTrackingDomain].
	TrackingDomain           string     `json:"tracking_domain"`
	TrackingDomainVerified   bool       `json:"tracking_domain_verified"`
	TrackingDomainVerifiedAt *time.Time `json:"tracking_domain_verified_at"`

	// AuthState summarizes SPF/DKIM/DMARC for the sending domain, as refreshed
	// by the background sweep: [AuthStatePassing], [AuthStateFailing] or
	// [AuthStateUnknown]. A sustained failing state gates cold sending and
	// warmup; see AuthFailingSince.
	AuthState string `json:"auth_state"`
	AuthSPF   bool   `json:"auth_spf"`
	// AuthDKIM is advisory: DKIM selectors are not discoverable from DNS, so a
	// missing DKIM never forces a failing verdict on its own.
	AuthDKIM        bool       `json:"auth_dkim"`
	AuthDMARC       bool       `json:"auth_dmarc"`
	AuthDMARCPolicy string     `json:"auth_dmarc_policy,omitempty"`
	AuthReason      string     `json:"auth_reason,omitempty"`
	AuthCheckedAt   *time.Time `json:"auth_checked_at,omitempty"`
	// AuthFailingSince is when the domain entered [AuthStateFailing], and nil
	// otherwise. The grace period runs from here, so a resolver hiccup or a
	// record broken minutes ago never stops sending immediately. Fix the DNS
	// and call [EmailService.RefreshAuthCheck] to clear it right away.
	AuthFailingSince *time.Time `json:"auth_failing_since,omitempty"`

	// Warmup is the warmup ramp anchor; a nil value means warmup is disabled.
	Warmup *time.Time `json:"warmup"`
	// WarmupPausedAt is when warmup was paused, when it is currently paused.
	WarmupPausedAt  *time.Time `json:"warmup_paused_at"`
	WarmupBase      int        `json:"warmup_base"`
	WarmupMax       int        `json:"warmup_max"`
	WarmupIncrease  int        `json:"warmup_increase"`
	WarmupReplyRate int        `json:"warmup_reply_rate"`
	WarmupTag       string     `json:"warmup_tag"`
	// WarmupPoolType is the partner pool this mailbox warms in: "free" or
	// "premium".
	WarmupPoolType string `json:"warmup_pool_type"`
	// WarmupStartTime and WarmupEndTime bound the daily warmup window as
	// "HH:MM" in the mailbox timezone.
	WarmupStartTime string `json:"warmup_start_time"`
	WarmupEndTime   string `json:"warmup_end_time"`
	// WarmupDays is a bitmask of the weekdays warmup runs on.
	WarmupDays int `json:"warmup_days"`

	// Timezone is the mailbox's own IANA zone, which its sending behavior and
	// business-hours window are evaluated in. Empty means not configured, so
	// only the campaign's own window applies.
	Timezone string `json:"timezone"`

	// Tags holds the ids of the tags applied to this mailbox.
	Tags []string `json:"tags"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Email is a connected email account (mailbox) as returned by the API.

func (*Email) WarmupActive

func (e *Email) WarmupActive() bool

WarmupActive reports whether warmup is enabled and running.

func (*Email) WarmupEnabled

func (e *Email) WarmupEnabled() bool

WarmupEnabled reports whether warmup is enabled for this mailbox, whether or not it is currently paused.

func (*Email) WarmupPaused

func (e *Email) WarmupPaused() bool

WarmupPaused reports whether warmup is enabled but paused. A paused mailbox keeps its ramp progress.

type EmailAccountUsage

type EmailAccountUsage struct {
	Total      int `json:"total"`
	Active     int `json:"active"`
	InWarmup   int `json:"in_warmup"`
	WithErrors int `json:"with_errors"`
}

EmailAccountUsage counts mailboxes by state.

type EmailListParams

type EmailListParams struct {
	ListOptions
	// Query is a free-text search over the mailbox address and name.
	Query string
	// Tag filters to mailboxes carrying the given tag id. It must be a UUID;
	// anything else is rejected with a 400.
	Tag string
}

EmailListParams filters and paginates a list of connected mailboxes. The page size defaults to 50 when Limit is zero.

type EmailService

type EmailService service

EmailService manages connected email accounts (mailboxes), their warmup configuration, their sending-domain authentication, their humanlike sending behavior and the one-off messages sent through them.

func (*EmailService) Allowance added in v0.3.0

func (s *EmailService) Allowance(ctx context.Context, opts ...RequestOption) (*MailboxAllowance, *Response, error)

Allowance reports how many mailboxes the workspace holds, how many it may hold and why, and any open request for more. Read it before a connect so a refusal is never a surprise after the credentials were typed: every connect path fails with a 403 carrying ErrCodeMailboxAllowanceReached once Remaining is zero. Requires an organization to be selected.

func (*EmailService) AppealWarmupBan

func (s *EmailService) AppealWarmupBan(ctx context.Context, id string, params *WarmupAppealParams, opts ...RequestOption) (*WarmupAppealResult, *Response, error)

AppealWarmupBan submits an appeal against a mailbox's warmup ban.

func (*EmailService) AuthCheck

func (s *EmailService) AuthCheck(ctx context.Context, id string, opts ...RequestOption) (*DomainAuthCheck, *Response, error)

AuthCheck runs a live SPF/DKIM/DMARC lookup against the mailbox's sending domain. It is read-only: it reports what DNS says right now and leaves the mailbox's stored Email.AuthState alone. Use EmailService.RefreshAuthCheck to record the verdict.

func (*EmailService) Behavior added in v0.3.0

func (s *EmailService) Behavior(ctx context.Context, id string, opts ...RequestOption) (*SendingBehavior, *Response, error)

Behavior returns the mailbox's sending-behavior profile, substituting the defaults for a mailbox that has never been configured so the result is always a complete profile. Deployments without the behavior engine answer with a 400.

func (*EmailService) BehaviorPlan added in v0.3.0

func (s *EmailService) BehaviorPlan(ctx context.Context, id string, opts ...RequestOption) (*DailyPlan, *Response, error)

BehaviorPlan returns the workday the mailbox rolled for the current local date and how much of it is already spent.

func (*EmailService) BulkTag

func (s *EmailService) BulkTag(ctx context.Context, params *BulkTagParams, opts ...RequestOption) (*BulkTagResult, *Response, error)

BulkTag adds and removes tags across many mailboxes in one call.

func (*EmailService) ConnectSMTPIMAP

func (s *EmailService) ConnectSMTPIMAP(ctx context.Context, params *SMTPIMAPParams, opts ...RequestOption) (*Email, *Response, error)

ConnectSMTPIMAP connects a mailbox by its own credentials. The server dials both legs to validate them before storing anything, so a bad password fails here rather than silently at send time. An address that is already connected is refused with a 409.

func (*EmailService) ConnectSMTPIMAPBulk added in v0.3.0

func (s *EmailService) ConnectSMTPIMAPBulk(ctx context.Context, params *SMTPIMAPBulkParams, opts ...RequestOption) (*MailboxBulkResult, *Response, error)

ConnectSMTPIMAPBulk connects up to MaxSMTPIMAPBulkRows SMTP/IMAP mailboxes in one call, answered per row so one bad row never hides the others: the call itself succeeds with a 200 even when every row failed, so read MailboxBulkResult.Summary and each row's Status. Rows past the workspace's allowance fail with ErrCodeMailboxAllowanceReached before any credential is dialed. Re-sending a batch is safe: an already connected mailbox is MailboxBulkSkipped, never doubled, so no idempotency key is needed. Every credential is validated against its own server, so a large batch takes a few seconds per mailbox.

func (*EmailService) Delete

func (s *EmailService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete disconnects and removes a mailbox for good, together with its imported mail, warmup history, credentials and any send still scheduled for it. A campaign that was using it keeps running on its remaining senders. Set the mailbox inactive with EmailService.Update when you only want it to stop.

The worker syncing the mailbox is told to drop it before the record is removed. If that instruction cannot be delivered the call fails with a 503 carrying ErrCodeMailboxWorkerUnreachable and nothing is removed; the client's default retry policy already retries 5xx responses, so retry again in a moment if it still fails rather than assuming it worked.

func (*EmailService) FinishOAuth

func (s *EmailService) FinishOAuth(ctx context.Context, code, state string, opts ...RequestOption) (*Email, *Response, error)

FinishOAuth exchanges the authorization code for a connected mailbox. It completes both a first connect (answered 201 with the new mailbox) and a EmailService.ReauthOAuth round trip (answered 200 with the renewed one); check the Response status code to tell them apart.

func (*EmailService) Get

func (s *EmailService) Get(ctx context.Context, id string, opts ...RequestOption) (*Email, *Response, error)

Get retrieves a single mailbox by ID.

func (*EmailService) GetTrackingDomain added in v0.3.0

func (s *EmailService) GetTrackingDomain(ctx context.Context, id string, opts ...RequestOption) (*TrackingDomainStatus, *Response, error)

GetTrackingDomain returns the mailbox's stored tracking-domain state plus the CNAME target this install expects. It is read-only and does no DNS work, so it is safe to call on every render; Status is TrackingStatusPending for a domain that has not been re-resolved. Use EmailService.VerifyTrackingDomain for the live check.

func (*EmailService) Hold added in v0.3.0

Hold takes the mailbox out of campaign sending until EmailService.Release puts it back, moving it to SendLifecycleReserve. Warmup keeps running. This is the owner's decision: the automatic rest-and-resume logic never touches a held mailbox. Bodyless and idempotent, so holding an already held mailbox simply returns its current state. Installs without the lifecycle engine answer with a 409.

func (*EmailService) List

func (s *EmailService) List(ctx context.Context, params *EmailListParams, opts ...RequestOption) (*Page[Email], error)

List returns a page of connected mailboxes, newest first.

func (*EmailService) PauseWarmup

func (s *EmailService) PauseWarmup(ctx context.Context, id string, opts ...RequestOption) (*Email, *Response, error)

PauseWarmup pauses warmup without losing ramp progress. The mailbox stays in its pool, so its health keeps being tracked.

func (*EmailService) ReauthOAuth added in v0.3.0

func (s *EmailService) ReauthOAuth(ctx context.Context, id string, opts ...RequestOption) (*OAuthStartResult, *Response, error)

ReauthOAuth starts an OAuth round trip that renews the tokens of an existing Gmail or Outlook mailbox after the provider invalidated them (a password change, a revoked grant). The finish leg is the ordinary EmailService.FinishOAuth; the consent must be for the mailbox's own address, and signing in with a different account is refused rather than quietly connecting the wrong mailbox. A successful reconnect keeps every setting, its history and its warmup progress, and never counts against the allowance.

An SMTP/IMAP mailbox is refused with a 400 (use EmailService.UpdateSMTPIMAPCredentials), and a mailbox whose sign-in is held by Warmbly Cloud with a 409. Requires the manage-emails permission.

func (*EmailService) RefreshAuthCheck added in v0.3.0

func (s *EmailService) RefreshAuthCheck(ctx context.Context, id string, opts ...RequestOption) (*DomainAuthCheck, *Response, error)

RefreshAuthCheck runs the same lookup as EmailService.AuthCheck and records the verdict against every active mailbox on the sending domain, since authentication is a property of the domain rather than of one mailbox. This is how a mailbox blocked by the send gate is unblocked: fix the DNS records, call this, and cold sending and warmup resume on the next scheduled send instead of waiting for the daily background check.

It needs the write scope because recording the verdict is what lifts the gate. Bodyless and derived from public DNS with no caller input, so repeating it converges on the same stored state.

func (*EmailService) Release added in v0.3.0

func (s *EmailService) Release(ctx context.Context, id string, opts ...RequestOption) (*SendLifecycleState, *Response, error)

Release puts a held or resting mailbox back into automatic management. It lands in SendLifecycleActive, or straight in SendLifecycleResting when warmup is running and still reports the mailbox as throttled or worse, so a release never sends cold mail from a mailbox that warmup can see is struggling; the returned Reason says which. It is also the manual exit from an automatic rest. Bodyless and idempotent.

func (*EmailService) ResumeWarmup

func (s *EmailService) ResumeWarmup(ctx context.Context, id string, opts ...RequestOption) (*Email, *Response, error)

ResumeWarmup resumes a paused warmup, shifting the ramp anchor forward so progress continues at the same daily volume.

func (*EmailService) Send

func (s *EmailService) Send(ctx context.Context, id string, params *SendEmailParams, opts ...RequestOption) (*SendResult, *Response, error)

Send sends a one-off message from the given mailbox, dispatched through the mailbox's assigned worker. It requires the send-campaigns scope and an organization. Pass WithIdempotencyKey when you may retry, so a retried call cannot send twice.

func (*EmailService) StartOAuth

func (s *EmailService) StartOAuth(ctx context.Context, provider string, opts ...RequestOption) (*OAuthStartResult, *Response, error)

StartOAuth begins connecting a Gmail or Outlook mailbox and returns the URL to send the user to. Provider is ProviderGmail or ProviderOutlook.

The provider redirects to a Warmbly-hosted page that posts the code and state back to the opener; pass those to EmailService.FinishOAuth.

func (*EmailService) StartWarmup

func (s *EmailService) StartWarmup(ctx context.Context, id string, opts ...RequestOption) (*Email, *Response, error)

StartWarmup enables warmup for a mailbox, resuming from the existing ramp progress when it was previously paused, and seeds the warmup task chain immediately rather than waiting for the next reconciler pass.

func (*EmailService) StopWarmup

func (s *EmailService) StopWarmup(ctx context.Context, id string, opts ...RequestOption) (*Email, *Response, error)

StopWarmup disables warmup and clears ramp progress; a later start begins a fresh ramp. Use EmailService.PauseWarmup to keep the progress.

func (*EmailService) SyncStatus added in v0.3.0

func (s *EmailService) SyncStatus(ctx context.Context, id string, opts ...RequestOption) (*MailboxSync, *Response, error)

SyncStatus reports where the mailbox's import stands, whether fair use is holding it, and the budget it runs under. State is nil until the worker has reported once.

func (*EmailService) Update

func (s *EmailService) Update(ctx context.Context, id string, params *EmailUpdateParams, opts ...RequestOption) (*Email, *Response, error)

Update modifies a mailbox's settings. Changes apply to the next scheduled send, not retroactively.

func (*EmailService) UpdateBehavior added in v0.3.0

func (s *EmailService) UpdateBehavior(ctx context.Context, id string, params *SendingBehaviorUpdateParams, opts ...RequestOption) (*SendingBehavior, *Response, error)

UpdateBehavior applies a partial update to the mailbox's sending-behavior profile and returns the whole profile. Nil fields keep their stored value. The body is the desired state, so a retry converges on the same profile and no idempotency key is needed.

func (*EmailService) UpdateSMTPIMAPCredentials added in v0.3.0

func (s *EmailService) UpdateSMTPIMAPCredentials(ctx context.Context, id string, params *SMTPIMAPCredentialsParams, opts ...RequestOption) (*Email, *Response, error)

UpdateSMTPIMAPCredentials replaces an existing SMTP/IMAP mailbox's credentials after a password or server change, validating them live before storing, then clears the authentication error and reactivates the mailbox on its existing worker so it resumes syncing from where it stopped. The address and display name never change. An OAuth mailbox is refused with a 400 (use EmailService.ReauthOAuth). Requires the manage-emails permission.

func (*EmailService) UpdateTrackingDomain

func (s *EmailService) UpdateTrackingDomain(ctx context.Context, id, domain string, opts ...RequestOption) (*TrackingDomainStatus, *Response, error)

UpdateTrackingDomain sets the mailbox's custom tracking subdomain (for example "t.acme.com"), resolves it once and records the verdict. An empty domain clears it and falls back to the shared tracking host.

The value is normalized before it is stored (scheme, path, trailing dot and case are stripped); anything that is still not a bare hostname (an IP, a host with a port, "localhost", a single label) is rejected with a 400. DNS can lag a freshly added record, so a miss is reported as unverified with a reason, not as an error. The record must point at TrackingDomainStatus.CNAMETarget.

func (*EmailService) Verify

func (s *EmailService) Verify(ctx context.Context, email string, opts ...RequestOption) (*VerifyResult, *Response, error)

Verify checks whether a recipient address is deliverable, before anything is ever sent to it, through whichever verifier the workspace uses (its connected provider, else the built-in syntax, MX and SMTP probe). Nothing is stored; use the contacts verification endpoints to record verdicts.

func (*EmailService) VerifyTrackingDomain added in v0.3.0

func (s *EmailService) VerifyTrackingDomain(ctx context.Context, id string, opts ...RequestOption) (*TrackingDomainStatus, *Response, error)

VerifyTrackingDomain re-resolves the mailbox's saved tracking domain and records the verdict without changing the domain itself. This is how a record that has finished propagating starts being used straight away; the backend also re-checks every custom domain hourly, so this is the impatient path rather than the only one. A transient resolver failure never revokes a verified domain. Bodyless and derived from public DNS, so it needs no idempotency key.

func (*EmailService) WarmupBanStatus

func (s *EmailService) WarmupBanStatus(ctx context.Context, id string, opts ...RequestOption) (*WarmupBanStatus, *Response, error)

WarmupBanStatus reports whether a mailbox has been blocked from the warmup pool, why, and whether the owner can appeal.

type EmailUpdateParams

type EmailUpdateParams struct {
	Name *string `json:"name,omitempty"`

	SignaturePlain *string `json:"signature_plain,omitempty"`
	SignatureHTML  *string `json:"signature_html,omitempty"`
	SignatureSync  *bool   `json:"signature_sync,omitempty"`
	SignatureCode  *bool   `json:"signature_code,omitempty"`

	// Status is [MailboxStatusActive], [MailboxStatusInactive] or
	// [MailboxStatusRevoked]. Setting a mailbox inactive stops its syncing and
	// sending within seconds while keeping its settings, history and worker
	// assignment; switching it back on resumes where it stopped.
	Status *string `json:"status,omitempty"`

	// CampaignLimit is the daily cold-campaign cap, 0 to 5000. 30 to 50 a day
	// is the safe band; a fresh mailbox should start at 10 to 20 and ramp.
	CampaignLimit *int `json:"campaign_limit,omitempty"`
	// MinWaitTime is the minimum gap between sends, in seconds.
	MinWaitTime *int    `json:"min_wait_time,omitempty"`
	ReplyTo     *string `json:"reply_to,omitempty"`

	// Timezone is the mailbox's own IANA zone, such as "America/Denver". Send
	// an empty string to clear it, which leaves only the campaign's own window
	// applying.
	Timezone *string `json:"timezone,omitempty"`

	// SaveToSent controls the Sent-folder copy on SMTP/IMAP mailboxes. Turn it
	// off when the submission server already files its own copy (Gmail,
	// Fastmail and Zoho do), or the folder ends up with two of everything.
	// Ignored for OAuth mailboxes.
	SaveToSent *bool `json:"save_to_sent,omitempty"`

	// Warmup enables or disables warmup. Prefer the explicit lifecycle methods
	// ([EmailService.StartWarmup] and friends) unless you are changing warmup
	// settings in the same call.
	Warmup          *bool   `json:"warmup,omitempty"`
	WarmupBase      *int    `json:"warmup_base,omitempty"`
	WarmupMax       *int    `json:"warmup_max,omitempty"`
	WarmupIncrease  *int    `json:"warmup_increase,omitempty"`
	WarmupReplyRate *int    `json:"warmup_reply_rate,omitempty"`
	WarmupTag       *string `json:"warmup_tag,omitempty"`
	WarmupStartTime *string `json:"warmup_start_time,omitempty"`
	WarmupEndTime   *string `json:"warmup_end_time,omitempty"`
	WarmupDays      *int    `json:"warmup_days,omitempty"`

	// Tags replaces the mailbox's tag ids wholesale when non-nil.
	Tags []string `json:"tags,omitempty"`
}

EmailUpdateParams are the parameters for updating a mailbox. Unset (nil) fields are left unchanged.

type EngagementBucket added in v0.3.0

type EngagementBucket struct {
	Key    string `json:"key"`
	Opens  int64  `json:"opens"`
	Clicks int64  `json:"clicks"`
}

EngagementBucket is one slice of an engagement breakdown: how many distinct contacts opened and clicked from that country, client or device.

type EngagementOrigin added in v0.3.0

type EngagementOrigin struct {
	Client         string `json:"client,omitempty"`
	DeviceType     string `json:"device_type,omitempty"`
	OS             string `json:"os,omitempty"`
	Browser        string `json:"browser,omitempty"`
	BrowserVersion string `json:"browser_version,omitempty"`
	CountryCode    string `json:"country_code,omitempty"`
	Region         string `json:"region,omitempty"`
	City           string `json:"city,omitempty"`
}

EngagementOrigin is what an open or click said about where it came from. Client names the mail client or image proxy when the user agent does (Gmail, Apple Mail, Outlook); the browser fields describe the rest. The location is resolved from the source network; the address itself is never stored. Every field is empty when unknown.

type EnterpriseInquiryParams

type EnterpriseInquiryParams struct {
	CompanyName  string `json:"company_name"`
	ContactName  string `json:"contact_name"`
	ContactEmail string `json:"contact_email"`
	// EstimatedVolume is expected monthly send volume.
	EstimatedVolume *int   `json:"estimated_volume,omitempty"`
	TeamSize        *int   `json:"team_size,omitempty"`
	Notes           string `json:"notes,omitempty"`
}

EnterpriseInquiryParams asks the sales team to get in touch. CompanyName, ContactName and ContactEmail are required.

type Error

type Error struct {
	// StatusCode is the HTTP status code of the response.
	StatusCode int `json:"-"`
	// Type is the machine-readable error family (the API "error" field).
	Type string `json:"error"`
	// Message is the human-readable description.
	Message string `json:"message"`
	// Code is a stable, machine-readable error code.
	Code string `json:"code"`
	// RequestID identifies the request server-side and should be included in
	// any support correspondence.
	RequestID string `json:"request_id"`
	// RetryAfter is the number of seconds to wait before retrying, when the
	// server provides it (typically on 429 responses).
	RetryAfter int `json:"retry_after,omitempty"`
	// Header is the full set of response headers, for inspection.
	Header http.Header `json:"-"`
}

Error is the typed error returned for any non-2xx API response. It mirrors the JSON error envelope returned by the Warmbly API, which is the same four fields on every endpoint — there is no nested "details" or per-field validation object to dig through:

{
  "error":      "Not Found",
  "message":    "Resource not found.",
  "code":       "not_found",
  "request_id": "0b6f...",
  "retry_after": 30
}

"error" and "message" are for people; branch on Error.Code and the HTTP status. "retry_after" appears only where the endpoint has a wait to quote (rate limiting); the client also fills it from the Retry-After header.

The package sentinels (ErrNotFound, ErrUnauthorized, ...) can be matched with errors.Is to branch on the HTTP status without parsing the body, and Error.HasCode on the stable code for the specific condition:

if errors.Is(err, warmbly.ErrNotFound) {
	// handle a 404
}

var apiErr *warmbly.Error
if errors.As(err, &apiErr) && apiErr.HasCode(warmbly.ErrCodeStorageLimitReached) {
	// the quota, not a malformed request
}
Example

Errors decode into a typed *Error that matches the package sentinels, so you can branch on the status without parsing the body.

package main

import (
	"context"
	"errors"
	"fmt"

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

func main() {
	client, _ := warmbly.New(warmbly.WithAPIKey("wmbly_..."))

	_, _, err := client.Campaigns.Get(context.Background(), "camp_missing")
	switch {
	case errors.Is(err, warmbly.ErrNotFound):
		fmt.Println("no such campaign")
	case errors.Is(err, warmbly.ErrRateLimited):
		var apiErr *warmbly.Error
		errors.As(err, &apiErr)
		fmt.Println("slow down for", apiErr.RetryAfter, "seconds")
	case err != nil:
		var apiErr *warmbly.Error
		if errors.As(err, &apiErr) {
			// The request id is what support needs to find the request.
			fmt.Printf("%s (request %s)\n", apiErr.Message, apiErr.RequestID)
		}
	}
}

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) HasCode added in v0.3.0

func (e *Error) HasCode(code string) bool

HasCode reports whether the response carried the given stable error code. It is nil-safe, so it can be used on the result of an errors.As that did not match:

var apiErr *warmbly.Error
errors.As(err, &apiErr)
if apiErr.HasCode(warmbly.ErrCodeNoOrganization) { ... }

Compare against the ErrCode* constants rather than literals: a code is stable where the human-readable message is not.

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether the error matches target. It enables matching against the package sentinels by HTTP status. The ErrServer sentinel matches any 5xx status.

func (*Error) Temporary

func (e *Error) Temporary() bool

Temporary reports whether the error is likely transient and worth retrying: rate limiting (429) or server errors (5xx).

It is a status-level guess. Two 5xx codes are not transient at all — ErrCodeMailboxProviderNotConfigured and, on the mailbox delete path, ErrCodeMailboxWorkerUnreachable — so check Error.HasCode before building a retry loop around them.

type EventSubscription

type EventSubscription struct {
	ID             string `json:"id"`
	ConnectionID   string `json:"connection_id"`
	OrganizationID string `json:"organization_id,omitempty"`
	// EventType is a webhook event key; see the Event* constants.
	EventType string `json:"event_type"`
	// Action is a provider action identifier from
	// [IntegrationCatalogEntry.ActionTypes].
	Action  string          `json:"action"`
	Config  json.RawMessage `json:"config,omitempty"`
	Enabled bool            `json:"enabled"`
	UseCase string          `json:"use_case,omitempty"`
	// AutomationID is set when the subscription is backed by an automation
	// flow rather than a single action.
	AutomationID *string   `json:"automation_id,omitempty"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
}

EventSubscription binds a Warmbly event to an action on a connection.

type EventSubscriptionParams

type EventSubscriptionParams struct {
	EventType string          `json:"event_type"`
	Action    string          `json:"action"`
	Config    json.RawMessage `json:"config,omitempty"`
	Enabled   *bool           `json:"enabled,omitempty"`
}

EventSubscriptionParams creates an event subscription.

type FeatureStatus

type FeatureStatus struct {
	Subscription     *SubscriptionStatus `json:"subscription"`
	CanSendCampaigns bool                `json:"can_send_campaigns"`
	CanUseWarmup     bool                `json:"can_use_warmup"`
	CanUseUnibox     bool                `json:"can_use_unibox"`
}

FeatureStatus is the subscription status plus the specific capability gates a client should check before offering an action. Every workspace may warm its mailboxes, so CanUseWarmup is only false where sending is blocked outright.

type FieldMapping

type FieldMapping struct {
	ID             string  `json:"id"`
	ConnectionID   string  `json:"connection_id"`
	OrganizationID string  `json:"organization_id,omitempty"`
	SubscriptionID *string `json:"subscription_id,omitempty"`
	// Direction is [SyncPush], [SyncPull] or [SyncBoth].
	Direction string `json:"direction,omitempty"`
	// ObjectName is the provider object the mapping applies to, for example
	// "contact".
	ObjectName    string `json:"object_name"`
	WarmblyField  string `json:"warmbly_field"`
	ExternalField string `json:"external_field"`
	// Transform names an optional value transformation.
	Transform string `json:"transform,omitempty"`
	// StaticValue writes a constant instead of reading a Warmbly field.
	StaticValue string `json:"static_value,omitempty"`
	// IsDefault marks a mapping the platform supplied rather than the user.
	IsDefault bool      `json:"is_default"`
	CreatedAt time.Time `json:"created_at"`
}

FieldMapping maps a Warmbly field to a field on the provider.

type FieldMappingInput

type FieldMappingInput struct {
	ExternalField string `json:"external_field"`
	WarmblyField  string `json:"warmbly_field,omitempty"`
	Transform     string `json:"transform,omitempty"`
	StaticValue   string `json:"static_value,omitempty"`
}

FieldMappingInput is one mapping in a wholesale replacement.

type FileUpload

type FileUpload struct {
	// Filename is the name recorded server-side. It is required.
	Filename string
	// Content is read to EOF and buffered so the request stays retryable.
	Content io.Reader
	// ContentType overrides the part's Content-Type. When empty the server
	// sniffs the file instead.
	ContentType string
}

FileUpload is a file being sent to a multipart endpoint such as campaign attachments or the organization avatar.

type Form added in v0.3.0

type Form struct {
	ID             string  `json:"id"`
	OrganizationID string  `json:"organization_id"`
	CreatedBy      *string `json:"created_by,omitempty"`
	// PublicID is the unguessable token in the hosted URL and the embed
	// codes. It survives a workspace export/import so installed embeds keep
	// working.
	PublicID string `json:"public_id"`
	// Name is the internal label shown in the dashboard, never to visitors.
	Name string `json:"name"`
	// Status is one of the FormStatus* constants.
	Status string `json:"status"`
	// Fields are the blocks in render order. A new form is seeded with first
	// name, last name and a required email.
	Fields []FormField `json:"fields"`
	Design FormDesign  `json:"design"`
	// SuccessMessage is shown after a submit unless RedirectURL is set.
	SuccessMessage string `json:"success_message"`
	// RedirectURL, when set, sends the visitor to your own thank-you page
	// instead of showing SuccessMessage.
	RedirectURL string `json:"redirect_url"`
	// CampaignID is the campaign new contacts are enrolled in on submit.
	// Sending still follows that campaign's own schedule and limits; a form
	// never causes immediate mail.
	CampaignID *string `json:"campaign_id,omitempty"`
	// CategoryIDs are the contact categories every submitted contact is
	// filed under.
	CategoryIDs []string `json:"category_ids"`
	// AllowedDomains restricts which sites may embed the form; a domain
	// covers its subdomains. Empty allows any site. The hosted link works
	// either way.
	AllowedDomains []string `json:"allowed_domains"`
	// CaptchaEnabled adds a Cloudflare Turnstile challenge. It only takes
	// effect when [FormsConfig.CaptchaAvailable] is true for the instance.
	CaptchaEnabled bool `json:"captcha_enabled"`

	// LogoURL, CoverURL and BackgroundURL are the public object URLs of the
	// uploaded brand assets; empty when none is set. Change them with
	// [FormService.UploadAsset] and [FormService.DeleteAsset], not Update.
	LogoURL       string `json:"logo_url"`
	CoverURL      string `json:"cover_url"`
	BackgroundURL string `json:"background_url"`

	// ViewsCount and SubmissionsCount are lifetime counters, kept forever.
	ViewsCount       int64      `json:"views_count"`
	SubmissionsCount int64      `json:"submissions_count"`
	LastSubmissionAt *time.Time `json:"last_submission_at,omitempty"`
	// PublishedAt is when the form last went from unpublished to published.
	PublishedAt *time.Time `json:"published_at,omitempty"`

	// StartsCount, IdentifiedCount and Trend are rollups over the last 14
	// days of funnel events. They are populated by [FormService.List] only;
	// other reads leave them zero and Trend nil. Trend has one entry per
	// day, oldest first.
	StartsCount     int64   `json:"starts_count"`
	IdentifiedCount int64   `json:"identified_count"`
	Trend           []int64 `json:"trend,omitempty"`

	// ShareURL is the hosted page URL, built on the workspace's verified
	// custom forms domain when it has one and the shared forms host
	// otherwise. Empty when the install has no forms host configured.
	ShareURL string `json:"share_url,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Form is a hosted lead-capture form: an ordered block list plus a theme, published at a public URL and embeddable on any site. A submission that carries an email creates or updates a contact, files it under CategoryIDs and, when CampaignID is set, enrolls it as a lead there.

type FormCreateParams added in v0.3.0

type FormCreateParams struct {
	// Name is required, at most 120 characters.
	Name string `json:"name"`
}

FormCreateParams names a new form. It is created as a draft seeded with first name, last name and email fields and the default success message; everything else is set with FormService.Update.

type FormDesign added in v0.3.0

type FormDesign struct {
	// FontFamily is one of the FormFont* constants.
	FontFamily       string `json:"font_family,omitempty"`
	PageBackground   string `json:"page_background,omitempty"`
	FormBackground   string `json:"form_background,omitempty"`
	TextColor        string `json:"text_color,omitempty"`
	LabelColor       string `json:"label_color,omitempty"`
	InputBackground  string `json:"input_background,omitempty"`
	InputBorderColor string `json:"input_border_color,omitempty"`
	InputTextColor   string `json:"input_text_color,omitempty"`
	PlaceholderColor string `json:"placeholder_color,omitempty"`
	AccentColor      string `json:"accent_color,omitempty"`
	ButtonBackground string `json:"button_background,omitempty"`
	ButtonTextColor  string `json:"button_text_color,omitempty"`
	// ButtonText is the submit button's caption.
	ButtonText string `json:"button_text,omitempty"`
	// ButtonSize is one of the FormSize* constants.
	ButtonSize      string `json:"button_size,omitempty"`
	ButtonFullWidth bool   `json:"button_full_width,omitempty"`
	// BorderRadius is clamped by the server to 0-24 pixels.
	BorderRadius *int `json:"border_radius,omitempty"`
	// MaxWidth is clamped by the server to 320-960 pixels (default 560).
	MaxWidth *int `json:"max_width,omitempty"`
	// Spacing is one of the FormSpacing* constants.
	Spacing string `json:"spacing,omitempty"`
	Shadow  *bool  `json:"shadow,omitempty"`
	// Theme records which preset seeded the current colors. Renderers never
	// read it, so any short lowercase slug is accepted.
	Theme string `json:"theme,omitempty"`
	// Layout is one of the FormLayout* constants.
	Layout string `json:"layout,omitempty"`
	// Mode is one of the FormMode* constants.
	Mode string `json:"mode,omitempty"`
	// PageBackgroundEnd turns the page background into a vertical gradient
	// from PageBackground to this color.
	PageBackgroundEnd string `json:"page_background_end,omitempty"`
	// Align is [FormAlignLeft] or [FormAlignCenter].
	Align string `json:"align,omitempty"`
	// ShowProgress shows a progress bar on multi-page forms.
	ShowProgress *bool `json:"show_progress,omitempty"`

	// BackgroundSize (a FormBackgroundSize* constant) and BackgroundOverlay
	// (0-100) style the uploaded background image: the overlay veils it with
	// the page color so text on top stays legible.
	BackgroundSize    string `json:"background_size,omitempty"`
	BackgroundOverlay *int   `json:"background_overlay,omitempty"`

	// The header is an optional bar carrying the logo and a title. A header
	// with neither is skipped on the live page. HeaderBackground and
	// HeaderSticky apply only to the page placement.
	HeaderEnabled    *bool  `json:"header_enabled,omitempty"`
	HeaderTitle      string `json:"header_title,omitempty"`
	HeaderBackground string `json:"header_background,omitempty"`
	// HeaderPlacement is one of the FormHeaderPlacement* constants.
	HeaderPlacement string `json:"header_placement,omitempty"`
	// HeaderAlign is [FormAlignLeft], [FormAlignCenter] or [FormAlignBetween].
	HeaderAlign    string `json:"header_align,omitempty"`
	HeaderSticky   *bool  `json:"header_sticky,omitempty"`

	// CoverTitle and CoverSubtitle are drawn over the split layout's cover
	// panel.
	CoverTitle    string `json:"cover_title,omitempty"`
	CoverSubtitle string `json:"cover_subtitle,omitempty"`

	// LogoSize is one of the FormSize* constants; LogoPosition one of the
	// FormLogoPosition* constants.
	LogoSize     string `json:"logo_size,omitempty"`
	LogoPosition string `json:"logo_position,omitempty"`
}

FormDesign is the theme rendered around the fields. Every value is optional and an unset one falls back to the renderer's default, so a sparse design is normal. Colors are "#rrggbb" or "transparent".

type FormField added in v0.3.0

type FormField struct {
	// ID is a builder-chosen slug (lowercase letters, digits, "_" and "-",
	// up to 40 characters), unique within the form and stable across edits.
	// Submissions key their answers by it.
	ID string `json:"id"`
	// Type is one of the FormFieldType* constants.
	Type string `json:"type"`
	// Label is required for every input type except hidden. For a page
	// break it is the page title.
	Label       string `json:"label"`
	Placeholder string `json:"placeholder,omitempty"`
	HelpText    string `json:"help_text,omitempty"`
	Required    bool   `json:"required"`
	// Options are the choices of a select, radio or checkboxes block (1 to
	// 50, each unique). Other types ignore them.
	Options []string `json:"options,omitempty"`
	// MapTo names the contact column the answer fills (a FormMapTo*
	// constant). Empty means the answer lands in the contact's custom fields
	// under the field's label.
	MapTo string `json:"map_to,omitempty"`
	// Value is the constant a hidden field submits, or the body text of a
	// paragraph block.
	Value string `json:"value,omitempty"`
	// Width is [FormFieldWidthFull] (the default) or [FormFieldWidthHalf].
	Width string `json:"width,omitempty"`
	// Rows is the visible height of a textarea (0 to 20; 0 uses the default).
	Rows int `json:"rows,omitempty"`
}

FormField is one block on a form.

type FormFunnelPage added in v0.3.0

type FormFunnelPage struct {
	// PageIndex is zero-based.
	PageIndex int `json:"page_index"`
	// Title is the page-break label, or a generated one when unset.
	Title         string `json:"title"`
	Reached       int64  `json:"reached"`
	CompletedFrom int64  `json:"completed_from"`
}

FormFunnelPage is one row of the page funnel: how many visitors reached the page, and how many of those went on to submit.

type FormIdentifiedVisitor added in v0.3.0

type FormIdentifiedVisitor struct {
	ContactID string    `json:"contact_id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	LastSeen  time.Time `json:"last_seen"`
	// FurthestPage is the zero-based index of the last page reached.
	FurthestPage int  `json:"furthest_page"`
	Completed    bool `json:"completed"`
	// Campaign names the campaign whose email brought this contact here,
	// when there was one.
	Campaign string `json:"campaign,omitempty"`
}

FormIdentifiedVisitor is a contact who opened the form through a personalized link, and how far they got.

type FormLink struct {
	URL string `json:"url"`
}

FormLink is a personalized form URL for one contact: the hosted page URL with a per-contact ticket (?t=...). Opening it pre-fills the contact's mapped fields and attributes the visit and any submission to that contact even if they type a different email. Holding the link is the identity, so treat it like an unsubscribe link.

type FormService added in v0.3.0

type FormService service

FormService manages hosted lead-capture forms: their definition and theme, their submissions and funnel analytics, per-contact personalized links, uploaded brand assets and the workspace-wide custom forms domain.

A form is built here but served elsewhere: the public page (/f/:public_id), its submit endpoint and the embed loader (/forms.js) live on the standalone forms host (FORMS_DOMAIN), take no authentication and are not part of this SDK. Form.ShareURL points at that host.

Form routes take the contact scopes (read_contacts to view, write_contacts to change), since a form exists only to create and update contacts. The /forms/domain routes additionally need the manage_settings organization permission for session (JWT) callers.

func (*FormService) Config added in v0.3.0

func (s *FormService) Config(ctx context.Context, opts ...RequestOption) (*FormsConfig, *Response, error)

Config reports the instance's forms deployment: the public forms origin and whether a captcha provider is available.

func (*FormService) Create added in v0.3.0

func (s *FormService) Create(ctx context.Context, params *FormCreateParams, opts ...RequestOption) (*Form, *Response, error)

Create makes a new draft form seeded with first name, last name and email fields. Configure and publish it with FormService.Update.

func (*FormService) Delete added in v0.3.0

func (s *FormService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete permanently removes a form together with its submissions, link tickets and funnel events. Contacts it created are kept. To take a form offline without losing anything, set its status to FormStatusArchived instead.

func (*FormService) DeleteAsset added in v0.3.0

func (s *FormService) DeleteAsset(ctx context.Context, id, kind string, opts ...RequestOption) (*Form, *Response, error)

DeleteAsset removes a brand image and returns the updated form. kind is one of the FormAsset* constants. Removing an asset that is not set succeeds.

func (*FormService) DeleteSubmission added in v0.3.0

func (s *FormService) DeleteSubmission(ctx context.Context, id, submissionID string, opts ...RequestOption) (*Response, error)

DeleteSubmission removes one submission. The contact it created or updated is kept.

func (*FormService) Domain added in v0.3.0

Domain reads the stored state of the workspace's custom forms domain without doing any DNS work. A stored but unverified domain reports FormsDomainStatusPending; use FormService.VerifyDomain for a live verdict.

func (*FormService) Get added in v0.3.0

func (s *FormService) Get(ctx context.Context, id string, opts ...RequestOption) (*Form, *Response, error)

Get retrieves a single form by ID.

func (*FormService) List added in v0.3.0

func (s *FormService) List(ctx context.Context, opts ...RequestOption) ([]Form, *Response, error)

List returns every form in the workspace. There is no pagination or filtering; a workspace holds at most 100 forms. This is the only read that fills the 14-day rollups (Form.StartsCount, Form.IdentifiedCount and Form.Trend).

func (*FormService) ListSubmissions added in v0.3.0

func (s *FormService) ListSubmissions(ctx context.Context, id string, params *FormSubmissionListParams, opts ...RequestOption) (*Page[FormSubmission], error)

ListSubmissions returns a page of the form's submissions, newest first. Paging is by timestamp rather than opaque cursor: the returned page's NextCursor is the RFC 3339 created_at of its last row, and Page.Next and Page.All pass it back as the before parameter. Pagination.Total is never set; use Form.SubmissionsCount for the lifetime count.

func (s *FormService) MintLink(ctx context.Context, id, contactID string, opts ...RequestOption) (*FormLink, *Response, error)

MintLink returns the personalized URL of the form for one contact. It is a GET on top of an upsert: the first call creates the contact's ticket and every later call returns the same one, so retries are safe. It needs the contact write scope, since minting writes a link row. It fails when the install has no forms host configured.

func (*FormService) SetDomain added in v0.3.0

func (s *FormService) SetDomain(ctx context.Context, domain string, opts ...RequestOption) (*FormsDomainStatus, *Response, error)

SetDomain stores a custom forms domain (a subdomain of the domain you send from, for example "forms.acme.com") and immediately resolves it, so saving and verifying are one step. An empty domain clears it back to the shared host. An unresolved record is not an error: the domain stays stored and unverified with a Status explaining why, and links keep using the shared host until it verifies. The server re-checks hourly on its own.

This is a workspace-wide setting; session callers need manage_settings.

func (*FormService) Stats added in v0.3.0

func (s *FormService) Stats(ctx context.Context, id, window string, opts ...RequestOption) (*FormStats, *Response, error)

Stats returns the form's funnel analytics over a trailing window. window is one of the FormStatsRange* constants; empty means FormStatsRange30Days.

func (*FormService) Update added in v0.3.0

func (s *FormService) Update(ctx context.Context, id string, params *FormUpdateParams, opts ...RequestOption) (*Form, *Response, error)

Update modifies a form. Edits to a published form go live immediately. Moving Status to published stamps Form.PublishedAt and requires at least one non-hidden input field.

func (*FormService) UploadAsset added in v0.3.0

func (s *FormService) UploadAsset(ctx context.Context, id, kind string, file *FileUpload, opts ...RequestOption) (*Form, *Response, error)

UploadAsset uploads a brand image and returns the updated form. kind is one of the FormAsset* constants; see them for the size caps. The file must be a PNG or JPG, sent as the multipart field "file". Uploading over an existing asset replaces it.

func (*FormService) VerifyDomain added in v0.3.0

func (s *FormService) VerifyDomain(ctx context.Context, opts ...RequestOption) (*FormsDomainStatus, *Response, error)

VerifyDomain re-resolves the stored custom forms domain and records the verdict. Call it after a DNS change instead of waiting for the hourly re-check. Session callers need manage_settings.

type FormStats added in v0.3.0

type FormStats struct {
	Totals FormStatsTotals `json:"totals"`
	// Daily has one entry per day of the window, oldest first.
	Daily []FormStatsDay `json:"daily"`
	// Pages is the page funnel; a single-page form has one row.
	Pages []FormFunnelPage `json:"pages"`
	// Sources, Countries, Devices and Campaigns are the top buckets (at most
	// eight each) keyed by referrer domain, ISO country code, device class
	// (desktop, mobile, tablet or unknown) and campaign name.
	Sources   []FormStatsBucket `json:"sources"`
	Countries []FormStatsBucket `json:"countries"`
	Devices   []FormStatsBucket `json:"devices"`
	Campaigns []FormStatsBucket `json:"campaigns"`
	// Identified lists the most recent contacts (up to 25) who opened the
	// form through a personalized link.
	Identified []FormIdentifiedVisitor `json:"identified"`
}

FormStats is the analytics payload for one form over a window: funnel totals, a daily series, per-page drop-off, traffic breakdowns and the contacts identified through personalized links. Location is resolved to country only; visitor IPs are never stored.

type FormStatsBucket added in v0.3.0

type FormStatsBucket struct {
	Key   string `json:"key"`
	Count int64  `json:"count"`
}

FormStatsBucket is one row of a breakdown.

type FormStatsDay added in v0.3.0

type FormStatsDay struct {
	// Date is the calendar day as YYYY-MM-DD.
	Date        string `json:"date"`
	Views       int64  `json:"views"`
	Starts      int64  `json:"starts"`
	Submissions int64  `json:"submissions"`
}

FormStatsDay is one day of the funnel series.

type FormStatsTotals added in v0.3.0

type FormStatsTotals struct {
	Views int64 `json:"views"`
	// Starts counts visitors who interacted with a field.
	Starts      int64 `json:"starts"`
	Submissions int64 `json:"submissions"`
	// CompletionRate is submissions over views, 0 to 1.
	CompletionRate float64 `json:"completion_rate"`
	// IdentifiedVisitors counts distinct contacts seen through a
	// personalized link.
	IdentifiedVisitors int64 `json:"identified_visitors"`
}

FormStatsTotals are the funnel counts for the whole window.

type FormSubmission added in v0.3.0

type FormSubmission struct {
	ID             string `json:"id"`
	FormID         string `json:"form_id"`
	OrganizationID string `json:"organization_id"`
	// ContactID is the contact the submission created or updated, or nil
	// when the form has no email field.
	ContactID *string `json:"contact_id,omitempty"`
	// CampaignID is the campaign whose email carried the personalized link
	// the visitor arrived through, when there was one.
	CampaignID *string `json:"campaign_id,omitempty"`
	// Data holds every answer keyed by [FormField.ID]. A checkboxes block
	// decodes as a []any of strings; every other input as a string.
	Data map[string]any `json:"data"`
	// SourceURL is the page the form was submitted from (the host page for
	// an embed, the hosted page otherwise).
	SourceURL string    `json:"source_url"`
	CreatedAt time.Time `json:"created_at"`

	// ContactEmail, ContactName and CampaignName are display summaries of
	// the linked records, resolved at read time; empty when unlinked.
	ContactEmail string `json:"contact_email,omitempty"`
	ContactName  string `json:"contact_name,omitempty"`
	CampaignName string `json:"campaign_name,omitempty"`
}

FormSubmission is one public submit, kept verbatim.

type FormSubmissionListParams added in v0.3.0

type FormSubmissionListParams struct {
	// Limit is the page size, 1 to 100. Zero uses the server default of 50.
	Limit int
	// Before returns only submissions created strictly before this instant.
	// Pass the previous page's last CreatedAt to walk backwards in time;
	// [Page.Next] and [Page.All] do this for you.
	Before *time.Time
}

FormSubmissionListParams control FormService.ListSubmissions. Submissions are keyset-paginated newest first on created_at rather than by opaque cursor.

type FormSubmittedPayload added in v0.3.0

type FormSubmittedPayload struct {
	FormID   string `json:"form_id"`
	FormName string `json:"form_name"`
	// SubmissionID is the stored submission, which the forms REST endpoint
	// serves verbatim.
	SubmissionID string `json:"submission_id"`
	// Data is the answers keyed by field id. Values keep the type the field
	// collected, so a multi-select arrives as a list and a number as a number.
	Data map[string]any `json:"data"`
	// SourceURL is the page the form was submitted from, when the embed
	// reported one.
	SourceURL string `json:"source_url,omitempty"`

	// ContactID is set when the submission carried a usable email and so
	// created or matched a contact. The mapped contact columns ride alongside
	// it, flat, so an automation reads {{.contact_email}} without walking
	// Data.
	ContactID    string `json:"contact_id,omitempty"`
	ContactEmail string `json:"contact_email,omitempty"`
	FirstName    string `json:"first_name,omitempty"`
	LastName     string `json:"last_name,omitempty"`
	Company      string `json:"company,omitempty"`
	Phone        string `json:"phone,omitempty"`
	// CampaignID is the campaign the form enrolls submitters into, when it
	// does.
	CampaignID string `json:"campaign_id,omitempty"`
}

FormSubmittedPayload is the EventFormSubmitted body.

Unlike the realtime gateway's form event, which carries ids only, the webhook carries the answers: the delivery is already authenticated to one endpoint by its signature, so there is no permission left to enforce at read time.

type FormUpdateParams added in v0.3.0

type FormUpdateParams struct {
	Name *string `json:"name,omitempty"`
	// Status moves the form through its lifecycle (FormStatus* constants).
	// Publishing requires at least one non-hidden input field.
	Status *string `json:"status,omitempty"`
	// Fields replaces the block list. Field IDs must stay stable across
	// edits or existing submissions lose their labels.
	Fields *[]FormField `json:"fields,omitempty"`
	// Design replaces the whole theme; it is not merged with the stored one.
	Design *FormDesign `json:"design,omitempty"`
	// SuccessMessage is capped at 2000 characters; an empty string resets it
	// to the server default.
	SuccessMessage *string `json:"success_message,omitempty"`
	// RedirectURL must be an absolute http(s) URL; an empty string clears it.
	RedirectURL *string `json:"redirect_url,omitempty"`
	// CampaignID enrolls new contacts in a campaign on submit. To detach the
	// form from its campaign set ClearCampaign instead: a nil pointer means
	// "leave as is".
	CampaignID *string `json:"campaign_id,omitempty"`
	// ClearCampaign sends campaign_id as JSON null, detaching the campaign.
	// It wins over CampaignID when both are set.
	ClearCampaign  bool      `json:"-"`
	CategoryIDs    *[]string `json:"category_ids,omitempty"`
	AllowedDomains *[]string `json:"allowed_domains,omitempty"`
	CaptchaEnabled *bool     `json:"captcha_enabled,omitempty"`
}

FormUpdateParams is the PATCH payload for FormService.Update. Nil fields are left untouched. Fields, Design, CategoryIDs and AllowedDomains replace their whole value when set (send the complete list or theme, not a diff), so an empty slice clears the list.

func (FormUpdateParams) MarshalJSON added in v0.3.0

func (p FormUpdateParams) MarshalJSON() ([]byte, error)

MarshalJSON emits campaign_id as an explicit null when ClearCampaign is set, which is how the server distinguishes "detach" from "unchanged".

type FormsConfig added in v0.3.0

type FormsConfig struct {
	// BaseURL is the origin of the standalone forms host, for example
	// "https://forms.example.com"; empty when FORMS_DOMAIN is not configured,
	// in which case no form can be served.
	BaseURL string `json:"base_url"`
	// CaptchaAvailable reports whether the operator configured a captcha
	// provider, which [Form.CaptchaEnabled] needs to take effect.
	CaptchaAvailable bool `json:"captcha_available"`
}

FormsConfig describes what this instance can do with forms, so a builder never offers a switch that cannot work.

type FormsDomainStatus added in v0.3.0

type FormsDomainStatus struct {
	// FormsDomain is the stored custom host, for example "forms.acme.com";
	// empty when none is set.
	FormsDomain           string     `json:"forms_domain"`
	FormsDomainVerified   bool       `json:"forms_domain_verified"`
	FormsDomainVerifiedAt *time.Time `json:"forms_domain_verified_at"`
	// CNAMETarget is the value to put in the CNAME record: this install's
	// shared forms host. Empty means the install has none, so nothing can
	// verify.
	CNAMETarget string `json:"cname_target"`
	// Status is one of the FormsDomainStatus* constants.
	Status string `json:"status"`
	// Message is a human-readable explanation of Status.
	Message string `json:"message"`
	// Observed is what DNS actually returned for the domain, so a typo can
	// be spotted by comparing it with CNAMETarget.
	Observed string `json:"observed,omitempty"`
	// FormsHostUnresolvable reports that the install's own forms host does
	// not resolve, an operator problem rather than the customer's record.
	FormsHostUnresolvable bool `json:"forms_host_unresolvable"`
}

FormsDomainStatus is the state of the workspace's custom forms domain, shaped like TrackingDomainStatus. Only a verified domain is used to build form URLs; until then every link stays on the shared forms host.

type GatewayTicket

type GatewayTicket struct {
	// URL is the websocket endpoint with the ticket already embedded, so it can
	// be dialed as-is.
	URL string `json:"url"`
	// ExpiresIn is the ticket's lifetime in seconds.
	ExpiresIn float64 `json:"expires_in"`
}

GatewayTicket is a short-lived, single-use credential for opening a gateway connection from a browser session.

type Generation

type Generation struct {
	Text string `json:"text"`
	// CreditsCharged is what this call cost, including any usage overage;
	// CreditsRemaining is the workspace balance afterwards.
	CreditsCharged   int    `json:"credits_charged"`
	CreditsRemaining int    `json:"credits_remaining"`
	TokensUsed       int    `json:"tokens_used"`
	Model            string `json:"model"`
}

Generation is the result of an AI writing call.

type GenerationService

type GenerationService service

GenerationService writes and rewrites campaign copy with AI, grounded in the workspace voice profile and any enabled AISkill playbooks.

Every call here spends AI credits and returns the remaining balance, so a caller can surface cost without a second request. None of them send anything.

func (*GenerationService) AIVariable

func (s *GenerationService) AIVariable(ctx context.Context, params *AIVariableParams, opts ...RequestOption) (*Generation, *Response, error)

AIVariable previews an AI variable block against a single contact. A prompt that renders empty against the contact costs nothing.

func (*GenerationService) Edit

func (s *GenerationService) Edit(ctx context.Context, params *EditParams, opts ...RequestOption) (*Generation, *Response, error)

Edit rewrites a passage according to an instruction.

func (*GenerationService) Write

func (s *GenerationService) Write(ctx context.Context, params *WriteParams, opts ...RequestOption) (*Generation, *Response, error)

Write generates a new piece of copy.

type Group

type Group struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	Color string `json:"color"`
	// Position is the group's index in its ordered set.
	Position  int32     `json:"position"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Group is a user-scoped label: a campaign folder, a mailbox tag or a contact category. See GroupService.

type GroupCreateParams

type GroupCreateParams struct {
	Title string `json:"title"`
	Color string `json:"color,omitempty"`
}

GroupCreateParams creates a group.

type GroupOrder

type GroupOrder struct {
	ID       string `json:"id"`
	Position int32  `json:"position"`
}

GroupOrder is one group's position after a reorder.

type GroupService

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

GroupService manages the three ordered label sets that organize the rest of the workspace:

  • Folders group campaigns.
  • Tags group mailboxes, and are what a campaign's tag-based sender strategy selects on.
  • Categories group contacts, and double as unified-inbox conversation labels.

Reach them as client.Folders, client.Tags and client.Categories. They share this type because the three endpoints are identical apart from their path.

There is no list endpoint: the current set rides along on the caller's profile, in User.Folders, User.Tags and User.Categories. Read it with AuthService.Me.

func (*GroupService) Create

func (s *GroupService) Create(ctx context.Context, params *GroupCreateParams, opts ...RequestOption) (*Group, *Response, error)

Create adds a group, appended to the end of the set.

func (*GroupService) Delete

func (s *GroupService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes a group. What it labeled keeps existing, unlabeled.

func (*GroupService) Move

func (s *GroupService) Move(ctx context.Context, id string, position int32, opts ...RequestOption) ([]GroupOrder, *Response, error)

Move reorders a group to the given zero-based position and returns the new order of the whole set.

func (*GroupService) Update

func (s *GroupService) Update(ctx context.Context, id string, params *GroupUpdateParams, opts ...RequestOption) (*Group, *Response, error)

Update renames or recolors a group.

type GroupUpdateParams

type GroupUpdateParams struct {
	Title *string `json:"title,omitempty"`
	Color *string `json:"color,omitempty"`
}

GroupUpdateParams renames or recolors a group. Nil fields are unchanged.

type HourlyStat

type HourlyStat struct {
	// Hour is 0-23 in the organization timezone.
	Hour    int   `json:"hour"`
	Sent    int64 `json:"sent"`
	Opens   int64 `json:"opens"`
	Clicks  int64 `json:"clicks"`
	Replies int64 `json:"replies"`
}

HourlyStat is one hour of engagement within a day.

type Identity

type Identity struct {
	UserID    string `json:"user_id"`
	Email     string `json:"email"`
	Name      string `json:"name"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`

	OrganizationID   *string `json:"organization_id,omitempty"`
	OrganizationName string  `json:"organization_name,omitempty"`

	// AuthType is [AuthTypeAPIKey], [AuthTypeOAuth] or [AuthTypeJWT].
	AuthType string `json:"auth_type"`
	// Scopes lists the granted API scope names for an API key or OAuth token.
	// It is empty for a session, which carries organization role permissions
	// instead.
	Scopes []string `json:"scopes"`
}

Identity is who the current credential is and what workspace it acts on. Every credential can read it, which makes it the right call for an integration to validate a connection and label it.

type ImportColumnMapping

type ImportColumnMapping struct {
	Index int `json:"index"`
	// Target is one of the ImportTarget* constants, or "custom:<key>".
	Target string `json:"target"`
	// CustomKey is the custom field an [ImportTargetCustom] column writes to.
	// It may use letters, numbers, underscores, spaces and dashes.
	CustomKey string `json:"custom_key,omitempty"`
	// VerificationProvider names the vocabulary of an
	// [ImportTargetVerificationStatus] column (a VerificationProvider*
	// constant). Empty recognizes each value by itself.
	VerificationProvider string `json:"verification_provider,omitempty"`
}

ImportColumnMapping maps the column at Index to a contact field. The index is zero-based and matches ContactImportPreview.Columns.

type ImportRowError

type ImportRowError struct {
	Line   int      `json:"line"`
	Email  string   `json:"email,omitempty"`
	Values []string `json:"values,omitempty"`
	Reason string   `json:"reason"`
}

ImportRowError is a row that could not be imported. Line is the 1-based index into the source file, after the header when there was one.

type InstanceInfo added in v0.3.0

type InstanceInfo struct {
	SelfHosted bool `json:"self_hosted"`
	// Version is the release tag and Commit the short commit it was built
	// from.
	Version string `json:"version,omitempty"`
	Commit  string `json:"commit,omitempty"`
	// UpdateAvailable is true when Latest is newer than Version.
	UpdateAvailable bool `json:"update_available"`
	// Latest is the newest published release, when the instance has checked.
	Latest *InstanceRelease `json:"latest,omitempty"`
	// CheckedAt is when the instance last looked for a release.
	CheckedAt *time.Time `json:"checked_at,omitempty"`
}

InstanceInfo is which Warmbly a self-hosted instance runs and whether a newer release exists. A hosted deployment answers SelfHosted false and nothing else. Applying an update is an admin-panel action, not an API call.

type InstanceRelease added in v0.3.0

type InstanceRelease struct {
	Tag         string    `json:"tag"`
	HTMLURL     string    `json:"html_url,omitempty"`
	PublishedAt time.Time `json:"published_at,omitempty"`
}

InstanceRelease is one published release.

type IntegrationCatalogEntry

type IntegrationCatalogEntry struct {
	// Provider is one of the Provider* constants.
	Provider string `json:"provider"`
	Name     string `json:"name"`
	Tagline  string `json:"tagline,omitempty"`
	// Category groups the provider: "crm", "automation", "notifications",
	// "meetings", "data" or "verification".
	Category string `json:"category,omitempty"`
	DocsURL  string `json:"docs_url,omitempty"`
	// AuthMethod is "oauth", "api_key" or "webhook".
	AuthMethod string `json:"auth_method"`
	BadgeColor string `json:"badge_color,omitempty"`
	Beta       bool   `json:"beta"`
	// WebhookHint is shown for webhook-URL and inbound providers.
	WebhookHint string `json:"webhook_hint,omitempty"`
	// Highlights are short "what you get" bullets.
	Highlights []string `json:"highlights,omitempty"`
	// Scopes are the OAuth scopes requested at authorize time.
	Scopes []string `json:"scopes,omitempty"`
	// Events are the Warmbly events this provider can react to, and
	// ActionTypes the actions it can actually perform.
	Events      []string `json:"events,omitempty"`
	ActionTypes []string `json:"action_types,omitempty"`
	// SupportsPush reports whether contacts can be pushed to this provider on
	// demand.
	SupportsPush bool `json:"supports_push"`
	// Configured reports whether this deployment has credentials for the
	// provider, so an unconfigured one can be greyed out rather than failing
	// at connect time.
	Configured bool `json:"configured"`
	// Capability is the provider's raw capability descriptor, when it has one.
	Capability json.RawMessage `json:"capability,omitempty"`
}

IntegrationCatalogEntry describes one connectable provider.

type IntegrationConnection

type IntegrationConnection struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	// Provider is one of the Provider* constants.
	Provider string `json:"provider"`
	Label    string `json:"label"`
	// Status is one of the Connection* constants.
	Status string `json:"status"`
	// AuthMethod is "oauth", "api_key" or "webhook".
	AuthMethod string `json:"auth_method"`
	// DisplayFields are the non-secret connection details worth showing.
	DisplayFields json.RawMessage `json:"display_fields,omitempty"`
	// ConfigCapabilities is the per-connection capability snapshot: selected
	// objects, enabled use cases, picker selections. It never holds secrets.
	ConfigCapabilities json.RawMessage `json:"config_capabilities,omitempty"`
	// SyncDirection is [SyncPush], [SyncPull] or [SyncBoth].
	SyncDirection string `json:"sync_direction"`

	ConnectedByUserID   *string    `json:"connected_by_user_id,omitempty"`
	ExternalAccountID   string     `json:"external_account_id,omitempty"`
	ExternalAccountName string     `json:"external_account_name,omitempty"`
	GrantedScopes       []string   `json:"granted_scopes,omitempty"`
	TokenExpiresAt      *time.Time `json:"token_expires_at,omitempty"`

	// Health is "unknown", "healthy", "degraded" or "down".
	Health          string     `json:"health"`
	HealthDetail    *string    `json:"health_detail,omitempty"`
	HealthCheckedAt *time.Time `json:"health_checked_at,omitempty"`

	LastSyncedAt *time.Time `json:"last_synced_at,omitempty"`
	LastError    *string    `json:"last_error,omitempty"`
	LastErrorAt  *time.Time `json:"last_error_at,omitempty"`

	// InboundWebhookURL is where an inbound provider should POST. It embeds a
	// rotatable per-connection secret, so treat it as a credential.
	InboundWebhookURL string `json:"inbound_webhook_url,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

IntegrationConnection is one connected third-party account.

type IntegrationService

type IntegrationService service

IntegrationService manages third-party connections: the provider catalog, connected accounts, the events each connection reacts to, field mappings, sync history and on-demand contact pushes.

Connecting an OAuth provider is a browser flow. Start it with IntegrationService.StartOAuth, send the user to the returned URL, and finish it with IntegrationService.FinishOAuth once the popup posts the code back. Those two calls are session-only.

func (*IntegrationService) Bookings

func (s *IntegrationService) Bookings(ctx context.Context, opts ...RequestOption) ([]Meeting, *Response, error)

Bookings returns meetings from connected scheduling providers. It is the integration-scoped view of the same data MeetingService.List returns.

func (*IntegrationService) Catalog

Catalog returns every connectable provider.

func (*IntegrationService) Connect

Connect creates a connection for an API-key or webhook provider.

func (*IntegrationService) Connection

func (s *IntegrationService) Connection(ctx context.Context, id string, opts ...RequestOption) (*ConnectionDetail, *Response, error)

Connection returns one connection with its event subscriptions and recent sync runs.

func (*IntegrationService) Connections

Connections returns the workspace's connected accounts.

func (*IntegrationService) CreateEvent

CreateEvent subscribes a connection to a Warmbly event.

func (*IntegrationService) DeleteEvent

func (s *IntegrationService) DeleteEvent(ctx context.Context, id, eventID string, opts ...RequestOption) (*Response, error)

DeleteEvent removes an event subscription.

func (*IntegrationService) Disconnect

func (s *IntegrationService) Disconnect(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Disconnect removes a connection and its stored credentials.

func (*IntegrationService) Events

Events returns a connection's event subscriptions.

func (*IntegrationService) FieldMappings

func (s *IntegrationService) FieldMappings(ctx context.Context, id string, opts ...RequestOption) ([]FieldMapping, *Response, error)

FieldMappings returns a connection's field mappings.

func (*IntegrationService) FinishOAuth

func (s *IntegrationService) FinishOAuth(ctx context.Context, code, state string, opts ...RequestOption) (*IntegrationConnection, *Response, error)

FinishOAuth exchanges the authorization code the provider redirected back with for a stored connection.

func (*IntegrationService) Push

func (s *IntegrationService) Push(ctx context.Context, id string, contactIDs []string, opts ...RequestOption) (*PushResult, *Response, error)

Push sends the given contacts to the provider now, rather than waiting for an event to fire.

func (*IntegrationService) ReauthOAuth

func (s *IntegrationService) ReauthOAuth(ctx context.Context, id string, opts ...RequestOption) (*OAuthStartResult, *Response, error)

ReauthOAuth restarts consent for a connection whose token expired or was revoked, returning a fresh URL to send the user to.

func (*IntegrationService) ReplaceFieldMappings

func (s *IntegrationService) ReplaceFieldMappings(ctx context.Context, id, object string, mappings []FieldMappingInput, opts ...RequestOption) ([]FieldMapping, *Response, error)

ReplaceFieldMappings replaces the mappings for one provider object wholesale.

func (*IntegrationService) Runs

func (s *IntegrationService) Runs(ctx context.Context, id string, opts ...RequestOption) ([]SyncRun, *Response, error)

Runs returns a connection's recent sync runs.

func (*IntegrationService) StartOAuth

func (s *IntegrationService) StartOAuth(ctx context.Context, provider, label string, opts ...RequestOption) (*OAuthStartResult, *Response, error)

StartOAuth begins the consent flow for an OAuth provider and returns the URL to send the user to. The label, if given, names the resulting connection.

func (*IntegrationService) Test

func (s *IntegrationService) Test(ctx context.Context, id string, opts ...RequestOption) (bool, *Response, error)

Test sends a test message through the connection to confirm it works. For a notification provider this posts a real message to the configured channel.

func (*IntegrationService) UpdateConfig

UpdateConfig changes a connection's non-secret configuration.

func (*IntegrationService) WebhookSecret

WebhookSecret returns the signing material an inbound provider needs. Treat the secret as a credential.

type Invitation

type Invitation struct {
	ID             string       `json:"id"`
	OrganizationID string       `json:"organization_id"`
	Email          string       `json:"email"`
	Role           string       `json:"role"`
	RoleID         *string      `json:"role_id,omitempty"`
	Roles          []MemberRole `json:"roles,omitempty"`
	Permissions    uint16       `json:"permissions"`
	InvitedBy      string       `json:"invited_by"`
	ExpiresAt      time.Time    `json:"expires_at"`
	CreatedAt      time.Time    `json:"created_at"`

	Organization  *Organization `json:"organization,omitempty"`
	InvitedByUser *User         `json:"invited_by_user,omitempty"`
}

Invitation is a pending invitation to join an organization. The invite token is never returned in a listing; fetch it with OrganizationService.InvitationLink.

func (*Invitation) Expired

func (i *Invitation) Expired() bool

Expired reports whether the invitation has lapsed.

type InvitationPreview added in v0.3.0

type InvitationPreview struct {
	OrganizationName   string `json:"organization_name"`
	OrganizationAvatar string `json:"organization_avatar,omitempty"`
	// InviterName is who sent it, when the API knows.
	InviterName string `json:"inviter_name,omitempty"`
	// Email is the address the invitation was issued to; only that address can
	// accept it.
	Email string `json:"email"`
	// Roles are the roles the invitee would land in.
	Roles []MemberRole `json:"roles"`
	// Expired is true when the invitation has lapsed and can no longer be
	// accepted.
	Expired bool `json:"expired"`
}

InvitationPreview is the public view of an invitation, resolved from its token before the recipient commits to joining. It deliberately carries only what a human needs to decide: no ids, no permission bitmask, and not the token itself.

type InviteMemberParams

type InviteMemberParams struct {
	Email string `json:"email"`
	// RoleIDs are the roles the invitee lands in; at least one is required.
	RoleIDs []string `json:"role_ids,omitempty"`
	// RoleID is a single-role shorthand for RoleIDs.
	RoleID *string `json:"role_id,omitempty"`
}

InviteMemberParams invites someone by email into one or more roles.

type LeadSyncConnection

type LeadSyncConnection struct {
	Connected  bool                       `json:"connected"`
	Connection *LeadSyncConnectionSummary `json:"connection"`
}

LeadSyncConnection reports whether the workspace has a Google account connected for lead sync.

type LeadSyncConnectionSummary

type LeadSyncConnectionSummary struct {
	ID                  string `json:"id"`
	ExternalAccountName string `json:"external_account_name"`
	// Status is one of the Connection* constants.
	Status string `json:"status"`
}

LeadSyncConnectionSummary identifies the connected Google account.

type LeadSyncCreateParams

type LeadSyncCreateParams struct {
	ConnectionID string `json:"connection_id"`
	SheetID      string `json:"sheet_id"`
	SheetTitle   string `json:"sheet_title,omitempty"`
	TabTitle     string `json:"tab_title,omitempty"`
	HasHeader    bool   `json:"has_header"`

	// ColumnMapping is validated when the source is saved, with the same
	// rules as a file import: it must be non-empty and map an email column,
	// and every target must be a known one. A bad mapping is a 400 here
	// rather than a failed sync later.
	ColumnMapping []ImportColumnMapping `json:"column_mapping"`
	// Dedup is one of the ImportDedup* constants.
	Dedup string `json:"dedup,omitempty"`

	TargetCampaignID  *string  `json:"target_campaign_id,omitempty"`
	CategoryIDs       []string `json:"category_ids,omitempty"`
	SubscribedDefault *bool    `json:"subscribed_default,omitempty"`
	Label             string   `json:"label,omitempty"`
}

LeadSyncCreateParams saves a new source. ConnectionID, SheetID and a non-empty ColumnMapping are required.

type LeadSyncResult

type LeadSyncResult struct {
	SourceID string               `json:"source_id"`
	Result   *ContactImportResult `json:"result"`
}

LeadSyncResult is what a manual sync returns: the import counts plus the source they belong to.

type LeadSyncService

type LeadSyncService service

LeadSyncService manages on-demand Google Sheets to contacts sync: a saved binding between a spreadsheet tab and the contact importer, re-runnable with "sync now". New rows create contacts and rows matching an existing contact by email update it.

The Google account itself is connected through the ordinary integration OAuth flow with provider ProviderGoogleSheets; see IntegrationService.StartOAuth.

Setting up a source runs in three steps: LeadSyncService.Spreadsheet to list a workbook's tabs, LeadSyncService.Preview to read the columns and get a suggested mapping, then LeadSyncService.Create to save it. Preview returns the same shape as the contact importer, so one column mapper serves both.

func (*LeadSyncService) Connection

func (s *LeadSyncService) Connection(ctx context.Context, opts ...RequestOption) (*LeadSyncConnection, *Response, error)

Connection reports whether a Google account is connected for lead sync.

func (*LeadSyncService) Create

Create saves a new sync source.

func (*LeadSyncService) Delete

func (s *LeadSyncService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes a saved sync source. The contacts it imported stay.

func (*LeadSyncService) Get

Get retrieves a saved sync source.

func (*LeadSyncService) Preview

func (s *LeadSyncService) Preview(ctx context.Context, connectionID, sheetID, tabTitle string, opts ...RequestOption) (*ContactImportPreview, *Response, error)

Preview reads the top rows of a tab and returns the same shape as ContactService.ImportPreview, including a suggested column mapping.

func (*LeadSyncService) Sources

func (s *LeadSyncService) Sources(ctx context.Context, opts ...RequestOption) ([]LeadSyncSource, *Response, error)

Sources returns the workspace's saved sync sources.

func (*LeadSyncService) SourcesForCampaign added in v0.3.0

func (s *LeadSyncService) SourcesForCampaign(ctx context.Context, campaignID string, opts ...RequestOption) ([]LeadSyncSource, *Response, error)

SourcesForCampaign returns the saved sync sources that enroll their leads in the given campaign.

func (*LeadSyncService) Spreadsheet

func (s *LeadSyncService) Spreadsheet(ctx context.Context, connectionID, sheetID string, opts ...RequestOption) (*Spreadsheet, *Response, error)

Spreadsheet returns a workbook's title and tabs, so a tab can be chosen before mapping columns.

func (*LeadSyncService) Sync

Sync runs a source now and returns the import counts. It runs in the request, so a large sheet takes a while.

func (*LeadSyncService) Update

Update edits a saved sync source.

type LeadSyncSource

type LeadSyncSource struct {
	ID              string `json:"id"`
	OrganizationID  string `json:"organization_id"`
	CreatedByUserID string `json:"created_by_user_id"`
	// Provider is the source system; today always [ProviderGoogleSheets].
	Provider string `json:"provider"`
	// ConnectionID is the integration connection the sheet is read through.
	ConnectionID string `json:"connection_id"`

	SheetID    string `json:"sheet_id"`
	SheetTitle string `json:"sheet_title,omitempty"`
	TabTitle   string `json:"tab_title,omitempty"`
	// A1Range narrows the read to a range within the tab.
	A1Range   string `json:"a1_range,omitempty"`
	HasHeader bool   `json:"has_header"`

	// ColumnMapping and Dedup are the contact-importer settings the rows flow
	// through, so a sheet sync behaves exactly like a file import.
	ColumnMapping []ImportColumnMapping `json:"column_mapping"`
	// Dedup is one of the ImportDedup* constants.
	Dedup string `json:"dedup"`

	// TargetCampaignID, when set, enrolls every new or updated lead in that
	// campaign on each sync.
	TargetCampaignID  *string  `json:"target_campaign_id,omitempty"`
	CategoryIDs       []string `json:"category_ids"`
	SubscribedDefault bool     `json:"subscribed_default"`

	Label string `json:"label,omitempty"`
	// Status is one of the LeadSync* constants.
	Status string `json:"status"`

	LastSyncedAt *time.Time           `json:"last_synced_at,omitempty"`
	LastResult   *ContactImportResult `json:"last_result,omitempty"`
	LastError    string               `json:"last_error,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

LeadSyncSource is a saved sheet-to-contacts binding.

type LeadSyncUpdateParams

type LeadSyncUpdateParams struct {
	SheetID    *string `json:"sheet_id,omitempty"`
	SheetTitle *string `json:"sheet_title,omitempty"`
	TabTitle   *string `json:"tab_title,omitempty"`
	HasHeader  *bool   `json:"has_header,omitempty"`

	// ColumnMapping, when non-nil, replaces the mapping and is validated like
	// on create; an empty mapping is a 400.
	ColumnMapping *[]ImportColumnMapping `json:"column_mapping,omitempty"`
	Dedup         *string                `json:"dedup,omitempty"`

	TargetCampaignID *string `json:"target_campaign_id,omitempty"`
	// ClearCampaign detaches the target campaign. Use it instead of a nil
	// TargetCampaignID, which means "leave unchanged".
	ClearCampaign     bool      `json:"clear_campaign,omitempty"`
	CategoryIDs       *[]string `json:"category_ids,omitempty"`
	SubscribedDefault *bool     `json:"subscribed_default,omitempty"`
	Label             *string   `json:"label,omitempty"`
}

LeadSyncUpdateParams edits a saved source. Nil fields are left unchanged.

type LimitRequest

type LimitRequest struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	// Field names the ceiling being raised: one of the LimitField*
	// constants.
	Field string `json:"field"`
	// CurrentEffective is what the limit was when the request was filed, so a
	// reviewer sees what the asker was looking at.
	CurrentEffective int    `json:"current_effective"`
	Requested        int    `json:"requested"`
	Reason           string `json:"reason"`
	// Status is one of the LimitRequest* constants.
	Status      string    `json:"status"`
	SubmittedBy string    `json:"submitted_by"`
	SubmittedAt time.Time `json:"submitted_at"`

	ReviewedBy  *string    `json:"reviewed_by,omitempty"`
	ReviewedAt  *time.Time `json:"reviewed_at,omitempty"`
	ReviewNotes string     `json:"review_notes,omitempty"`
}

LimitRequest is an ask for a higher plan ceiling, pending review.

type LimitRequestParams

type LimitRequestParams struct {
	Field     string `json:"field"`
	Requested int    `json:"requested"`
	Reason    string `json:"reason"`
}

LimitRequestParams asks for a higher ceiling. Field is one of the LimitField* constants, Requested must exceed the current effective limit, and Reason (up to 2000 characters) is required. A mailbox request on a workspace whose allowance is unlimited is refused.

type LimitsAndCounts

type LimitsAndCounts struct {
	Limits *OrganizationLimits `json:"limits"`
	Counts *OrganizationCounts `json:"counts"`
	// Mailboxes is how many mailboxes the workspace may hold and why.
	Mailboxes *MailboxAllowance `json:"mailboxes,omitempty"`
	// Storage is attachment storage against its quota.
	Storage *StorageUsage `json:"storage,omitempty"`
}

LimitsAndCounts pairs the enforced ceilings with current usage, plus the two meters that have no plan column of their own: the mailbox allowance and attachment storage.

type ListOptions

type ListOptions struct {
	// Limit is the maximum number of items per page (server-capped, typically
	// at 100). Zero uses the server default.
	Limit int `json:"-"`
	// Cursor is the opaque pagination token from a previous page's NextCursor.
	Cursor string `json:"-"`
}

ListOptions are the pagination controls common to every list endpoint. Resource-specific list parameter types embed it. They always travel in the query string, so they are never serialized into a request body even when embedded in a POST-search parameter type.

type LoginParams

type LoginParams struct {
	Email     string `json:"email"`
	Password  string `json:"password"`
	Turnstile string `json:"turnstile,omitempty"`

	// ReferralCode is the ?ref= code a signup arrived with, so the new
	// workspace is attributed to its referrer.
	ReferralCode string `json:"referral_code,omitempty"`
	// Invite is a team-invitation token. On a deployment that runs
	// invite-only registration ([AuthConfig.InvitesRequired]) it is what
	// permits the signup, and the account lands in the inviting workspace
	// rather than a new one. It must resolve to a live invitation for the same
	// email, or the request fails with code "invitation_invalid"; omitting it
	// on a closed deployment fails with "registration_invite_only" or
	// "registration_closed".
	Invite string `json:"invite,omitempty"`
}

LoginParams starts a sign-in or a signup. Turnstile carries a bot-check token when the deployment requires one (AuthConfig.Captcha); the last two fields only matter to AuthService.Register.

type MCPServer

type MCPServer struct {
	ID    string `json:"id"`
	OrgID string `json:"org_id"`
	Name  string `json:"name"`
	URL   string `json:"url"`
	// AuthType is "none" or "bearer". The token itself is sealed server-side
	// and never returned.
	AuthType string `json:"auth_type"`
	Enabled  bool   `json:"enabled"`
	// DiscoveredTools is what the server advertised on the last refresh.
	DiscoveredTools []MCPTool `json:"discovered_tools"`
	// LastError is why the most recent refresh failed, when it did.
	LastError string    `json:"last_error,omitempty"`
	CreatedBy *string   `json:"created_by,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

MCPServer is an external MCP server whose tools the assistant may call.

type MCPServerParams

type MCPServerParams struct {
	Name string `json:"name"`
	URL  string `json:"url"`
	// AuthType is "none" or "bearer".
	AuthType string `json:"auth_type,omitempty"`
	// Token is the bearer credential, sealed server-side on receipt.
	Token string `json:"token,omitempty"`
}

MCPServerParams connects an MCP server.

type MCPServerUpdateParams

type MCPServerUpdateParams struct {
	Name    *string `json:"name,omitempty"`
	Enabled *bool   `json:"enabled,omitempty"`
	// Token replaces the stored credential.
	Token *string `json:"token,omitempty"`
}

MCPServerUpdateParams updates a connected server. Nil fields are unchanged.

type MCPTool

type MCPTool struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	// InputSchema is the tool's JSON Schema.
	InputSchema json.RawMessage `json:"input_schema,omitempty"`
}

MCPTool is one tool discovered on a connected MCP server.

type MailboxAllowance added in v0.3.0

type MailboxAllowance struct {
	// Used is the number of mailboxes connected right now.
	Used int `json:"used"`
	// Allowance is the cap; nil means unlimited.
	Allowance *int `json:"allowance"`
	// Remaining is Allowance minus Used, never negative; nil when unlimited.
	Remaining *int `json:"remaining"`
	// Basis is one of the MailboxAllowance* constants.
	Basis string `json:"basis"`
	// SendsPerMailbox is the fair-use divisor.
	SendsPerMailbox int `json:"sends_per_mailbox"`
	// PlanDailySends is the plan's daily send cap when it has one.
	PlanDailySends *int   `json:"plan_daily_sends,omitempty"`
	PlanName       string `json:"plan_name,omitempty"`
	// Paid is false for a free workspace, whose path to more mailboxes is a
	// plan rather than a limit-increase request.
	Paid bool `json:"paid"`
	// PendingRequest is the open limit-increase request for mailboxes (a
	// [LimitRequest] on the "max_email_accounts" field), if any. File one with
	// [OrganizationService.RequestLimitIncrease].
	PendingRequest *LimitRequest `json:"pending_request,omitempty"`
}

MailboxAllowance is how many mailboxes a workspace holds, how many it may hold, and why. Mailboxes are unlimited on every paid plan in principle; the fair-use allowance is one mailbox per daily send the plan includes, which is far more than safe sending ever needs. Nothing is ever removed for being over the allowance: a workspace that moves to a smaller plan keeps every mailbox and simply cannot add more.

func (*MailboxAllowance) CanAdd added in v0.3.0

func (a *MailboxAllowance) CanAdd(n int) bool

CanAdd reports whether n more mailboxes fit.

func (*MailboxAllowance) Unlimited added in v0.3.0

func (a *MailboxAllowance) Unlimited() bool

Unlimited reports whether the workspace may connect any number of mailboxes.

type MailboxBulkResult added in v0.3.0

type MailboxBulkResult struct {
	Data    []MailboxBulkRow   `json:"data"`
	Summary MailboxBulkSummary `json:"summary"`
	// Allowance is the workspace's mailbox allowance after the batch, so a
	// caller can tell how many more rows will fit without another call.
	Allowance *MailboxAllowance `json:"allowance,omitempty"`
}

MailboxBulkResult is the per-row answer to a bulk connect.

type MailboxBulkRow added in v0.3.0

type MailboxBulkRow struct {
	// Row is the zero-based index of the row in the request, so failed lines
	// can be handed back to whoever supplied them.
	Row   int    `json:"row"`
	Email string `json:"email"`
	// Status is [MailboxBulkConnected], [MailboxBulkSkipped] or
	// [MailboxBulkFailed].
	Status string `json:"status"`
	// Code is the stable error code for a skipped or failed row:
	// "already_connected" for a skip, [ErrCodeMailboxAllowanceReached] for a
	// row past the allowance, otherwise the code the single connect would have
	// returned.
	Code    string `json:"code,omitempty"`
	Message string `json:"message,omitempty"`
	// ID is the new mailbox id for a connected row.
	ID *string `json:"id,omitempty"`
}

MailboxBulkRow is one row's answer from a bulk connect.

type MailboxBulkSummary added in v0.3.0

type MailboxBulkSummary struct {
	Total     int `json:"total"`
	Connected int `json:"connected"`
	Skipped   int `json:"skipped"`
	Failed    int `json:"failed"`
}

MailboxBulkSummary counts a bulk connect's rows by outcome.

type MailboxCredentials

type MailboxCredentials struct {
	Username string `json:"username"`
	Password string `json:"password"`
	Host     string `json:"host"`
	// Port is any port from 1 to 65535.
	Port int `json:"port"`
	// Security is [MailSecurityTLS] or [MailSecurityStartTLS]. Leave it empty
	// to let the port decide (tls for SMTP 465 and IMAP 993, starttls for SMTP
	// 587 and IMAP 143); set it for anything non-standard, such as a
	// submission relay on 2525. A server expecting STARTTLS looks unreachable
	// to a client attempting implicit TLS, and vice versa.
	Security string `json:"security,omitempty"`
}

MailboxCredentials are the host, port and login for one leg of an SMTP/IMAP connection.

type MailboxSync added in v0.3.0

type MailboxSync struct {
	// State is nil until the worker has reported once.
	State  *MailboxSyncState `json:"state"`
	Policy MailboxSyncPolicy `json:"policy"`
}

MailboxSync is a mailbox's sync progress together with the budget it runs under.

type MailboxSyncCursor added in v0.3.0

type MailboxSyncCursor struct {
	// PageToken is Gmail's messages.list continuation.
	PageToken string `json:"page_token,omitempty"`
	// Folders is the per-folder position for IMAP and Microsoft Graph.
	Folders map[string]MailboxSyncFolderCursor `json:"folders,omitempty"`
}

MailboxSyncCursor is the resumable position of a mailbox's backfill.

type MailboxSyncFolderCursor added in v0.3.0

type MailboxSyncFolderCursor struct {
	// Next is an opaque continuation (Microsoft Graph nextLink).
	Next string `json:"next,omitempty"`
	// UID is the lowest IMAP UID already imported; the walk continues below it.
	UID uint32 `json:"uid,omitempty"`
	// Done marks the folder exhausted for this window.
	Done bool `json:"done,omitempty"`
}

MailboxSyncFolderCursor is the resumable position inside one folder of a backfill. It is opaque operational detail, surfaced for diagnostics only.

type MailboxSyncPolicy added in v0.3.0

type MailboxSyncPolicy struct {
	// BackfillDays is how far back the initial import reaches (90 by default).
	BackfillDays int `json:"backfill_days"`
	// BackfillMessages caps how many messages the initial import stores
	// (5000 by default).
	BackfillMessages int `json:"backfill_messages"`
	// DailyMessages caps new (live) messages stored per UTC day. Replies to
	// the mailbox's own sends have a separate budget of the same size.
	DailyMessages int `json:"daily_messages"`
	// OrgDailyMessages caps new plus backfilled messages stored across the
	// whole organization per UTC day.
	OrgDailyMessages int `json:"org_daily_messages"`
}

MailboxSyncPolicy is the fair-use budget a mailbox syncs under. Mail over a budget is not dropped: it waits on the server with the cursor held and comes in when the window rolls.

type MailboxSyncState added in v0.3.0

type MailboxSyncState struct {
	// BackfillStatus is [SyncBackfillPending], [SyncBackfillRunning] or
	// [SyncBackfillComplete].
	BackfillStatus string            `json:"backfill_status"`
	BackfillCursor MailboxSyncCursor `json:"backfill_cursor"`
	// BackfillSynced counts messages the import has stored so far.
	BackfillSynced int `json:"backfill_synced"`
	// BackfillSince is the cutoff the running import uses, fixed at start so
	// a later settings change does not move the goalposts mid-walk.
	BackfillSince       *time.Time `json:"backfill_since,omitempty"`
	BackfillStartedAt   *time.Time `json:"backfill_started_at,omitempty"`
	BackfillCompletedAt *time.Time `json:"backfill_completed_at,omitempty"`

	// ThrottledUntil is set while fair use is deferring live mail; nil when
	// the mailbox is within budget.
	ThrottledUntil *time.Time `json:"throttled_until,omitempty"`
	// ThrottleReason names the exhausted budget, one of the SyncThrottle*
	// constants.
	ThrottleReason string `json:"throttle_reason,omitempty"`
	// Deferred counts live messages currently waiting on budget: seen on the
	// server but not yet stored. It drops back to zero once they are admitted.
	Deferred int `json:"deferred"`

	LastSyncedAt *time.Time `json:"last_synced_at,omitempty"`
}

MailboxSyncState is what the platform knows about a mailbox's sync: backfill progress, whether fair use is holding it, and when it last ran.

func (*MailboxSyncState) Throttled added in v0.3.0

func (s *MailboxSyncState) Throttled(now time.Time) bool

Throttled reports whether fair use is currently deferring live mail.

type Meeting

type Meeting struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	// Source is the scheduling provider, or "manual" for one logged by hand.
	Source string `json:"source"`
	// ExternalEventID is the provider's identifier for the booking.
	ExternalEventID string `json:"external_event_id,omitempty"`
	// Status is one of the Meeting* constants.
	Status       string `json:"status"`
	InviteeEmail string `json:"invitee_email"`
	InviteeName  string `json:"invitee_name"`
	EventName    string `json:"event_name"`
	EventType    string `json:"event_type,omitempty"`

	ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
	EndTime      *time.Time `json:"end_time,omitempty"`

	JoinURL       string `json:"join_url,omitempty"`
	Location      string `json:"location,omitempty"`
	CancelURL     string `json:"cancel_url,omitempty"`
	RescheduleURL string `json:"reschedule_url,omitempty"`
	// CanceledReason is set once the booking is canceled.
	CanceledReason string `json:"canceled_reason,omitempty"`

	ContactID  *string `json:"contact_id,omitempty"`
	CampaignID *string `json:"campaign_id,omitempty"`
	// ContactName is joined in for display.
	ContactName string `json:"contact_name,omitempty"`
	// RawPayload is the provider's original webhook body.
	RawPayload json.RawMessage `json:"raw_payload,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Meeting is one booked call.

type MeetingCreateParams

type MeetingCreateParams struct {
	Title        string `json:"title,omitempty"`
	InviteeName  string `json:"invitee_name,omitempty"`
	InviteeEmail string `json:"invitee_email"`
	// ScheduledFor is an RFC 3339 timestamp and is required.
	ScheduledFor    string `json:"scheduled_for,omitempty"`
	DurationMinutes int    `json:"duration_minutes,omitempty"`
	Location        string `json:"location,omitempty"`
	JoinURL         string `json:"join_url,omitempty"`
	ContactID       string `json:"contact_id,omitempty"`
}

MeetingCreateParams logs a meeting that was booked outside a connected provider. It is matched to a contact by InviteeEmail unless ContactID says otherwise. At least one of InviteeName and InviteeEmail is required, as is a parseable ScheduledFor; Title defaults to "Call".

A meeting logged this way is recorded with source "manual" and does not fire the "meeting booked" automations a provider booking would, so logging your own call never alerts you about it.

type MeetingListParams

type MeetingListParams struct {
	ListOptions
	// Timeframe is [MeetingsUpcoming] or [MeetingsPast]. Empty returns both.
	Timeframe string
	// Status is one of the Meeting* constants.
	Status string
	// Query is a free-text search over the invitee and the event name.
	Query string
}

MeetingListParams filters and paginates the meetings list.

type MeetingService

type MeetingService service

MeetingService lists meetings booked through a connected scheduling provider such as Calendly or Cal.com, and lets one be logged by hand.

func (*MeetingService) Create

func (s *MeetingService) Create(ctx context.Context, params *MeetingCreateParams, opts ...RequestOption) (*Meeting, *Response, error)

Create logs a meeting booked outside a connected provider.

func (*MeetingService) Delete

func (s *MeetingService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes a meeting record.

func (*MeetingService) List

func (s *MeetingService) List(ctx context.Context, params *MeetingListParams, opts ...RequestOption) (*Page[Meeting], error)

List returns a page of booked meetings.

func (*MeetingService) Summary

func (s *MeetingService) Summary(ctx context.Context, opts ...RequestOption) (*MeetingsSummary, *Response, error)

Summary returns the meeting counters.

type MeetingsSummary

type MeetingsSummary struct {
	Upcoming int `json:"upcoming"`
	Today    int `json:"today"`
	Total    int `json:"total"`
	Canceled int `json:"canceled"`
}

MeetingsSummary counts booked meetings for a header row.

type Member

type Member struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	UserID         string `json:"user_id"`
	// Role is the built-in role name, one of the Role* constants. Owner is a
	// membership flag rather than a role row.
	Role string `json:"role"`
	// RoleID is the member's first assigned role; Roles is the full set.
	RoleID *string      `json:"role_id,omitempty"`
	Roles  []MemberRole `json:"roles,omitempty"`
	// Permissions is the effective grant across every assigned role. Test it
	// with [Member.Can].
	Permissions uint16 `json:"permissions"`

	InvitedBy  *string    `json:"invited_by,omitempty"`
	InvitedAt  time.Time  `json:"invited_at"`
	AcceptedAt *time.Time `json:"accepted_at,omitempty"`

	User         *User         `json:"user,omitempty"`
	Organization *Organization `json:"organization,omitempty"`

	// Email and Name are flattened from the joined user for convenience.
	Email string `json:"email"`
	Name  string `json:"name"`
}

Member is a user's membership in an organization.

func (*Member) Can

func (m *Member) Can(perms uint16) bool

Can reports whether the member holds every bit in perms. The owner always holds every permission.

func (*Member) IsOwner

func (m *Member) IsOwner() bool

IsOwner reports whether the member owns the organization.

type MemberRole

type MemberRole struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Color string `json:"color"`
}

MemberRole is a lightweight role reference for rendering the roles a member holds, without the permission payload.

type MessageParams

type MessageParams struct {
	// MessageID deduplicates a retried send. Leave it empty and the server
	// generates one.
	MessageID string `json:"message_id,omitempty"`
	Text      string `json:"text"`
	// Page and Resource tell the assistant what the user is looking at, so
	// references like "this campaign" resolve.
	Page     string `json:"page,omitempty"`
	Resource string `json:"resource,omitempty"`
}

MessageParams sends a message to the assistant.

type MetaService

type MetaService service

MetaService reads the small, cross-cutting endpoints: who the current credential is, the plan catalog, and the timezone list.

func (*MetaService) GatewayTicket

func (s *MetaService) GatewayTicket(ctx context.Context, opts ...RequestOption) (*GatewayTicket, *Response, error)

GatewayTicket mints a single-use websocket ticket for the current session. This route is session-only.

func (*MetaService) Identity

func (s *MetaService) Identity(ctx context.Context, opts ...RequestOption) (*Identity, *Response, error)

Identity returns who the current credential is and which workspace it acts on. Any valid credential can call it, whatever its scopes.

func (*MetaService) Plans

func (s *MetaService) Plans(ctx context.Context, opts ...RequestOption) ([]Plan, *Response, error)

Plans returns the public plan catalog. On a deployment without a billing provider (AuthConfig.BillingEnabled false) the route is absent.

func (*MetaService) Realtime

func (s *MetaService) Realtime(ctx context.Context, opts ...RequestOption) (*RealtimeInfo, *Response, error)

Realtime returns the gateway's connection details. This route is session-only. For a programmatic gateway connection, authenticate with an API key holding PermRealtimeSubscribe; see the gateway subpackage.

func (*MetaService) Timezones

func (s *MetaService) Timezones(ctx context.Context, opts ...RequestOption) ([]Timezone, *Response, error)

Timezones returns the timezones a campaign schedule or mailbox can use.

type MiniCampaign

type MiniCampaign struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

MiniCampaign is a campaign reference embedded in another resource.

type MiniCategory

type MiniCategory struct {
	ID    string `json:"id"`
	Title string `json:"title"`
	Color string `json:"color"`
}

MiniCategory is a contact category (also surfaced as a conversation label in the unified inbox) embedded in another resource.

type NodePosition

type NodePosition struct {
	ID string  `json:"id"`
	X  float64 `json:"x"`
	Y  float64 `json:"y"`
}

NodePosition is one node's coordinates on the builder canvas.

type Notification

type Notification struct {
	ID             string  `json:"id"`
	UserID         string  `json:"user_id"`
	OrganizationID *string `json:"organization_id,omitempty"`
	// Category is one of the Notif* constants.
	Category string         `json:"category"`
	Title    string         `json:"title"`
	Body     string         `json:"body,omitempty"`
	Link     string         `json:"link,omitempty"`
	Metadata map[string]any `json:"metadata,omitempty"`
	// ReadAt is nil while the notification is unread.
	ReadAt    *time.Time `json:"read_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

Notification is one entry in the in-app feed.

type NotificationEmailDelivery added in v0.3.0

type NotificationEmailDelivery struct {
	MinMinutes int `json:"min_minutes"`
	MaxMinutes int `json:"max_minutes"`
	// DailyCap is the most notification emails one account receives a day.
	DailyCap int `json:"daily_cap"`
}

NotificationEmailDelivery is the deployment's bounds for the email channel, so a digest-window control renders the right range.

type NotificationFeed

type NotificationFeed struct {
	Notifications []Notification `json:"notifications"`
	Unread        int            `json:"unread"`
}

NotificationFeed is a page of the in-app feed plus the unread count.

type NotificationPreferences

type NotificationPreferences struct {
	InboundReply    CategoryPref `json:"inbound_reply"`
	InboundOOO      CategoryPref `json:"inbound_out_of_office"`
	HealthBounce    CategoryPref `json:"health_bounce"`
	HealthComplaint CategoryPref `json:"health_complaint"`
	WorkerDowntime  CategoryPref `json:"health_worker_downtime"`
	SecuritySignIn  CategoryPref `json:"security_new_signin"`
	BillingAlert    CategoryPref `json:"billing_alert"`
	TeamActivity    CategoryPref `json:"team_activity"`
	// CampaignPaused and DomainAuth default to on with email: both mean the
	// platform will (or did) stop sending, so they must reach someone who can
	// act.
	CampaignPaused CategoryPref `json:"campaign_paused"`
	DomainAuth     CategoryPref `json:"health_domain_auth"`

	// EmailDigestMinutes bundles pending notification emails into one send.
	// It must fall within [NotificationEmailDelivery.MinMinutes] and
	// [NotificationEmailDelivery.MaxMinutes]; zero on an update means the
	// server default. Security sign-in alerts always go out immediately
	// regardless.
	EmailDigestMinutes int `json:"email_digest_minutes"`
}

NotificationPreferences is the caller's full notification configuration. It is always returned fully populated.

type NotificationPreferencesResult

type NotificationPreferencesResult struct {
	Preferences   NotificationPreferences    `json:"preferences"`
	EmailDelivery *NotificationEmailDelivery `json:"email_delivery,omitempty"`
}

NotificationPreferencesResult is the preferences plus how email delivery is configured on this deployment.

type OAuth2Config

type OAuth2Config struct {
	// ClientID is the application's public client identifier.
	ClientID string
	// ClientSecret is the confidential client secret. Leave empty for public
	// clients that rely solely on PKCE.
	ClientSecret string
	// RedirectURL must exactly match one of the application's registered URIs.
	RedirectURL string
	// Scopes are the permission keys to request.
	Scopes []string
	// Endpoint overrides the authorization server URLs. Zero uses
	// [DefaultEndpoint].
	Endpoint OAuth2Endpoint
	// HTTPClient is used for token requests. Nil uses a sensible default.
	HTTPClient *http.Client
}

OAuth2Config configures the OAuth 2.1 authorization-code flow (with optional PKCE) for an application acting on behalf of a user.

A typical web flow:

cfg := &warmbly.OAuth2Config{
	ClientID:     "...",
	ClientSecret: "...",
	RedirectURL:  "https://app.example.com/callback",
	Scopes:       []string{"campaigns:read", "contacts:read"},
}
verifier := warmbly.GenerateVerifier()
url := cfg.AuthCodeURL(state, warmbly.S256ChallengeOption(verifier))
// ... redirect the user, receive ?code=... on the callback ...
tok, err := cfg.Exchange(ctx, code, warmbly.VerifierOption(verifier))
client, err := cfg.NewClient(ctx, tok)

func (*OAuth2Config) AuthCodeURL

func (c *OAuth2Config) AuthCodeURL(state string, opts ...AuthCodeOption) string

AuthCodeURL builds the URL to which the user should be redirected to grant authorization. state is an opaque, unguessable value echoed back to the redirect URI; verify it to defend against CSRF. Pass S256ChallengeOption to use PKCE (strongly recommended, and required for public clients).

func (*OAuth2Config) Exchange

func (c *OAuth2Config) Exchange(ctx context.Context, code string, opts ...AuthCodeOption) (*Token, error)

Exchange trades an authorization code for a token. When PKCE was used, pass VerifierOption with the same verifier supplied to S256ChallengeOption.

func (*OAuth2Config) NewClient

func (c *OAuth2Config) NewClient(ctx context.Context, t *Token, opts ...Option) (*Client, error)

NewClient builds a *Client authenticated with a refreshing token source seeded from t.

func (*OAuth2Config) Refresh

func (c *OAuth2Config) Refresh(ctx context.Context, refreshToken string) (*Token, error)

Refresh exchanges a refresh token for a fresh access token.

func (*OAuth2Config) Revoke

func (c *OAuth2Config) Revoke(ctx context.Context, token string) error

Revoke revokes an access or refresh token.

func (*OAuth2Config) TokenSource

func (c *OAuth2Config) TokenSource(ctx context.Context, t *Token) TokenSource

TokenSource returns a TokenSource that starts from t and transparently refreshes the access token using its refresh token as it expires. It is safe for concurrent use.

type OAuth2Endpoint

type OAuth2Endpoint struct {
	// AuthURL is the user-facing authorization (consent) URL.
	AuthURL string
	// TokenURL is the token endpoint (code exchange, refresh, client creds).
	TokenURL string
	// RevokeURL is the token revocation endpoint.
	RevokeURL string
}

OAuth2Endpoint holds the URLs of the Warmbly authorization server. The zero value falls back to DefaultEndpoint.

type OAuth2Error

type OAuth2Error struct {
	StatusCode  int    `json:"-"`
	Code        string `json:"error"`
	Description string `json:"error_description"`
	URI         string `json:"error_uri"`
	Message     string `json:"message"`
	RequestID   string `json:"request_id"`
	// Body is the raw error-response body for diagnostics. It is only populated
	// for non-2xx responses (never a successful token payload), so it does not
	// carry issued credentials.
	Body []byte `json:"-"`
}

OAuth2Error is returned when the token or revocation endpoint responds with an error. It carries both the RFC 6749 fields (Code/Description) and the Warmbly error envelope fields (Message/RequestID).

func (*OAuth2Error) Error

func (e *OAuth2Error) Error() string

type OAuthApp

type OAuthApp struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	CreatedBy      string `json:"created_by"`
	Name           string `json:"name"`
	Description    string `json:"description,omitempty"`
	LogoURL        string `json:"logo_url,omitempty"`
	WebsiteURL     string `json:"website_url,omitempty"`
	// ClientID is the public client identifier.
	ClientID string `json:"client_id"`
	// RedirectURIs are the exact URIs the authorization code may be returned
	// to. They are matched exactly, not by prefix.
	RedirectURIs []string `json:"redirect_uris"`
	// Scopes is the permission bitmask the app may request, built from the
	// Perm* constants.
	Scopes uint64 `json:"scopes"`

	// AllowedWebhookDomains constrains the host of any webhook endpoint the
	// app registers. A leading dot matches subdomains; without one the match
	// is exact. Empty means the app cannot register webhooks at all.
	AllowedWebhookDomains []string `json:"allowed_webhook_domains,omitempty"`
	// WebhookURL turns on the app-level subscription: every workspace that
	// authorizes the app delivers to this URL, scoped to what that workspace
	// granted. Its host must fall inside AllowedWebhookDomains.
	WebhookURL string `json:"webhook_url,omitempty"`
	// WebhookEvents narrows that subscription. Empty means every non-firehose
	// event the grant's scopes allow.
	WebhookEvents []string `json:"webhook_events,omitempty"`

	// Status is [OAuthAppActive] or [OAuthAppInactive].
	Status string `json:"status"`
	// IsPublic marks a client that authenticates with PKCE and no secret, such
	// as a native app or an MCP client. PKCE is mandatory for these.
	IsPublic bool `json:"is_public"`
	// DynamicallyRegistered is true for clients that self-registered through
	// the RFC 7591 endpoint.
	DynamicallyRegistered bool `json:"dynamically_registered"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

OAuthApp is a registered OAuth 2.1 application. The client secret is never included here; it is returned once by OAuthAppService.Create and OAuthAppService.RotateSecret.

type OAuthAppParams

type OAuthAppParams struct {
	Name         string   `json:"name"`
	Description  string   `json:"description,omitempty"`
	LogoURL      string   `json:"logo_url,omitempty"`
	WebsiteURL   string   `json:"website_url,omitempty"`
	RedirectURIs []string `json:"redirect_uris"`
	// Scopes is the permission bitmask, built from the Perm* constants.
	Scopes                uint64   `json:"scopes"`
	AllowedWebhookDomains []string `json:"allowed_webhook_domains,omitempty"`
	WebhookURL            string   `json:"webhook_url,omitempty"`
	WebhookEvents         []string `json:"webhook_events,omitempty"`
}

OAuthAppParams registers or replaces an OAuth application. The API rewrites the application from what you send rather than merging, so send the complete desired state on update.

type OAuthAppService

type OAuthAppService service

OAuthAppService registers and manages OAuth 2.1 applications: the client credentials, redirect URIs and scopes an integration uses to act on behalf of a Warmbly user, plus the app-level webhook subscription that comes with them.

This service administers applications. To act as an OAuth client — build an authorization URL, exchange a code, refresh a token — use OAuth2Config and ClientCredentialsConfig instead.

func (*OAuthAppService) Authorize

func (s *OAuthAppService) Authorize(ctx context.Context, params *AuthorizeParams, opts ...RequestOption) (string, *Response, error)

Authorize approves a consent request and returns the URL the browser should be sent to, carrying the authorization code back to the application.

func (*OAuthAppService) AuthorizeDetails

func (s *OAuthAppService) AuthorizeDetails(ctx context.Context, query url.Values, opts ...RequestOption) (*ConsentInfo, *Response, error)

AuthorizeDetails resolves an incoming authorization request into what a consent screen should show. Pass the query parameters the application sent the user with.

func (*OAuthAppService) AuthorizedApps

func (s *OAuthAppService) AuthorizedApps(ctx context.Context, opts ...RequestOption) ([]AuthorizedApp, *Response, error)

AuthorizedApps returns the applications the caller has granted access to the current workspace.

func (*OAuthAppService) Create

Create registers a new OAuth application. The returned OAuthAppWithSecret is the only time the client secret is available.

func (*OAuthAppService) Delete

func (s *OAuthAppService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete permanently removes an OAuth application, revoking every token issued under it.

func (*OAuthAppService) Get

func (s *OAuthAppService) Get(ctx context.Context, id string, opts ...RequestOption) (*OAuthApp, *Response, error)

Get retrieves a single OAuth application.

func (*OAuthAppService) List

func (s *OAuthAppService) List(ctx context.Context, opts ...RequestOption) ([]OAuthApp, *Response, error)

List returns the workspace's registered OAuth applications.

func (*OAuthAppService) RegisterDynamicClient

func (s *OAuthAppService) RegisterDynamicClient(ctx context.Context, params *DynamicClientParams, opts ...RequestOption) (*DynamicClient, *Response, error)

RegisterDynamicClient self-registers an OAuth client. The endpoint is open and unauthenticated but per-IP rate limited, and registration alone grants no access — a human still has to consent.

func (*OAuthAppService) RevokeAuthorizedApp

func (s *OAuthAppService) RevokeAuthorizedApp(ctx context.Context, applicationID string, opts ...RequestOption) (*Response, error)

RevokeAuthorizedApp withdraws the caller's grant to an application and revokes its tokens.

func (*OAuthAppService) RotateSecret

func (s *OAuthAppService) RotateSecret(ctx context.Context, id string, opts ...RequestOption) (string, *Response, error)

RotateSecret issues a new client secret and returns it. The previous secret stops working immediately, so deploy the new one before rotating.

func (*OAuthAppService) RotateWebhookSecret

func (s *OAuthAppService) RotateWebhookSecret(ctx context.Context, id string, opts ...RequestOption) (string, *Response, error)

RotateWebhookSecret issues a new app-webhook signing secret and returns it. The previous secret stops verifying immediately.

func (*OAuthAppService) Update

func (s *OAuthAppService) Update(ctx context.Context, id string, params *OAuthAppParams, opts ...RequestOption) (*OAuthApp, *Response, error)

Update replaces an OAuth application's registration. See OAuthAppParams.

func (s *OAuthAppService) UploadLogo(ctx context.Context, file *FileUpload, opts ...RequestOption) (string, *Response, error)

UploadLogo stores an app logo (PNG or JPEG, up to 2 MB) and returns its URL, which you then pass in OAuthAppParams.LogoURL. It is a separate call so a logo can be uploaded during registration, before the app has an id.

func (*OAuthAppService) WebhookDeliveries

func (s *OAuthAppService) WebhookDeliveries(ctx context.Context, id string, params *WebhookDeliveryListParams, opts ...RequestOption) (*Page[WebhookDelivery], error)

WebhookDeliveries returns a page of the app's delivery log, across every workspace that authorized it.

func (*OAuthAppService) WebhookEndpoints

func (s *OAuthAppService) WebhookEndpoints(ctx context.Context, id string, opts ...RequestOption) ([]Webhook, *Response, error)

WebhookEndpoints returns the per-workspace endpoints materialized from the app-level subscription, one for each workspace that authorized the app.

func (*OAuthAppService) WebhookSecret

func (s *OAuthAppService) WebhookSecret(ctx context.Context, id string, opts ...RequestOption) (string, *Response, error)

WebhookSecret returns the signing secret for the app-level webhook subscription. Verify deliveries with it exactly as for an ordinary endpoint; see VerifyWebhookSignature.

type OAuthAppWithSecret

type OAuthAppWithSecret struct {
	OAuthApp
	// ClientSecret cannot be retrieved again. Store it securely.
	ClientSecret string `json:"client_secret,omitempty"`
}

OAuthAppWithSecret is an OAuthApp together with its plaintext client secret, returned only at creation.

type OAuthStartResult

type OAuthStartResult struct {
	URL   string `json:"url"`
	State string `json:"state"`
}

OAuthStartResult is the provider consent URL plus the state value that ties the round trip together.

type OnboardingParams

type OnboardingParams struct {
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	// ReferralSource is how they found Warmbly.
	ReferralSource string `json:"referral_source,omitempty"`
	Role           string `json:"role,omitempty"`
	TeamSize       string `json:"team_size,omitempty"`
}

OnboardingParams completes the first-run questions. FirstName and LastName are required; the rest are validated against fixed sets, so an unexpected value is rejected rather than stored.

type Option

type Option func(*Client) error

Option configures a Client. Options are applied in order by New.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey authenticates the client with a Warmbly API key (prefixed "wmbly_"). This is the recommended scheme for server-to-server access.

func WithAccessToken

func WithAccessToken(token string) Option

WithAccessToken authenticates the client with a static OAuth 2.1 access token (prefixed "wmblyo_"). Prefer WithTokenSource when you have a refresh token and want transparent renewal.

func WithAuthenticator

func WithAuthenticator(a Authenticator) Option

WithAuthenticator sets a custom Authenticator, for advanced scenarios not covered by the built-in schemes.

func WithBaseURL

func WithBaseURL(raw string) Option

WithBaseURL overrides the API base URL (default https://api.warmbly.com/v1). Useful for targeting a self-hosted instance or a staging environment. The URL should include the version path segment.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient sets the underlying *http.Client. Use this to customize transport, proxies, TLS or timeouts. The client's Timeout, if any, applies to each individual request.

func WithHeader

func WithHeader(key, value string) Option

WithHeader sets a default header sent on every request. It may be called multiple times to set several headers.

func WithMaxRetries

func WithMaxRetries(n int) Option

WithMaxRetries sets how many times the client retries a request that fails with a 429 or 5xx response. Retries use exponential backoff with jitter and honor any Retry-After header. The default is 2; set 0 to disable retries.

func WithRetryWaitBounds

func WithRetryWaitBounds(minWait, maxWait time.Duration) Option

WithRetryWaitBounds sets the minimum and maximum backoff between retries.

func WithTokenSource

func WithTokenSource(src TokenSource) Option

WithTokenSource authenticates the client with a TokenSource, refreshing the access token transparently as it expires.

func WithUserAgent

func WithUserAgent(product string) Option

WithUserAgent appends a product token to the default User-Agent, so requests from your application are identifiable in Warmbly's logs.

type OrgArchiveInfo added in v0.3.0

type OrgArchiveInfo struct {
	FormatVersion    int              `json:"format_version"`
	SourceInstance   string           `json:"source_instance"`
	SourceAppVersion string           `json:"source_app_version"`
	OrganizationID   string           `json:"organization_id"`
	OrganizationName string           `json:"organization_name"`
	ExportedAt       time.Time        `json:"exported_at"`
	Groups           []string         `json:"groups"`
	HasSecrets       bool             `json:"has_secrets"`
	RowCounts        map[string]int64 `json:"row_counts"`
	BlobCount        int              `json:"blob_count"`
	Members          []OrgArchiveUser `json:"members"`
}

OrgArchiveInfo is the safe-to-show summary of an archive's manifest.

type OrgArchiveUser added in v0.3.0

type OrgArchiveUser struct {
	ID        string `json:"id"`
	Email     string `json:"email"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Role      string `json:"role"`
	IsOwner   bool   `json:"is_owner"`
}

OrgArchiveUser is a member carried by an archive. The importer matches these to destination accounts by email; there is no password material, so an archive can never mint a login.

type OrgDataGroupInfo added in v0.3.0

type OrgDataGroupInfo struct {
	// Key is one of the OrgDataGroup* constants.
	Key         string `json:"key"`
	Label       string `json:"label"`
	Description string `json:"description"`
	// Required groups cannot be switched off.
	Required bool `json:"required"`
	// Heavy marks the groups that dominate archive size on a busy workspace.
	Heavy bool `json:"heavy"`
	// Requires names the groups this one cannot travel without. Selecting a
	// group selects these too, on the server as well.
	Requires []string `json:"requires,omitempty"`
}

OrgDataGroupInfo describes one data group from the server's own catalog, so a picker renders from it rather than from a copy that drifts.

type OrgExportJob added in v0.3.0

type OrgExportJob struct {
	ID             string  `json:"id"`
	OrganizationID string  `json:"organization_id"`
	RequestedBy    *string `json:"requested_by,omitempty"`
	// Status is one of the OrgTransfer* constants.
	Status string `json:"status"`

	// Groups are the data groups the archive carries; IncludeSecrets whether
	// credentials were re-sealed into it.
	Groups         []string `json:"groups"`
	IncludeSecrets bool     `json:"include_secrets"`
	FormatVersion  int      `json:"format_version"`

	ProgressPercent int    `json:"progress_percent"`
	ProgressStage   string `json:"progress_stage"`

	// ArchiveBytes and ArchiveSHA256 describe the finished file; RowCounts is
	// per-table.
	ArchiveBytes  *int64           `json:"archive_bytes,omitempty"`
	ArchiveSHA256 *string          `json:"archive_sha256,omitempty"`
	RowCounts     map[string]int64 `json:"row_counts"`

	ErrorMessage *string    `json:"error_message,omitempty"`
	StartedAt    *time.Time `json:"started_at,omitempty"`
	CompletedAt  *time.Time `json:"completed_at,omitempty"`
	// ExpiresAt is when the archive is deleted from storage.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
	UpdatedAt time.Time  `json:"updated_at"`
}

OrgExportJob is one archive build. Poll OrganizationService.Export until OrgExportJob.Terminal; a completed job can be downloaded until ExpiresAt.

func (*OrgExportJob) Terminal added in v0.3.0

func (j *OrgExportJob) Terminal() bool

Terminal reports whether the job has stopped moving.

func (*OrgExportJob) TotalRows added in v0.3.0

func (j *OrgExportJob) TotalRows() int64

TotalRows sums every table's row count.

type OrgExportParams added in v0.3.0

type OrgExportParams struct {
	// Groups to include (OrgDataGroup* constants). Empty means every group;
	// [OrgDataGroupCore] is always included, and a group's requirements are
	// pulled in with it.
	Groups []string `json:"groups,omitempty"`
	// IncludeSecrets re-seals mailbox and integration credentials into the
	// archive under Passphrase, so the destination brings mailboxes back
	// without everyone reconnecting. It requires Passphrase.
	IncludeSecrets bool `json:"include_secrets,omitempty"`
	// Passphrase protects the secrets bundle; at least
	// [OrgTransferCatalog.MinPassphrase] characters. It is never stored: lose
	// it and the credentials in that archive are unrecoverable.
	Passphrase string `json:"passphrase,omitempty"`
}

OrgExportParams starts an archive build.

type OrgImportJob added in v0.3.0

type OrgImportJob struct {
	ID             string  `json:"id"`
	OrganizationID string  `json:"organization_id"`
	RequestedBy    *string `json:"requested_by,omitempty"`
	// Status is one of the OrgTransfer* constants.
	Status string `json:"status"`

	ArchiveBytes  *int64  `json:"archive_bytes,omitempty"`
	ArchiveSHA256 *string `json:"archive_sha256,omitempty"`
	// SourceManifest summarizes the archive that was applied.
	SourceManifest *OrgArchiveInfo `json:"source_manifest,omitempty"`

	Groups []string `json:"groups"`
	// ConflictStrategy is [OrgImportSkip] or [OrgImportOverwrite].
	ConflictStrategy string `json:"conflict_strategy"`

	ProgressPercent int    `json:"progress_percent"`
	ProgressStage   string `json:"progress_stage"`

	RowCounts map[string]int64 `json:"row_counts"`
	Warnings  []string         `json:"warnings"`

	ErrorMessage *string    `json:"error_message,omitempty"`
	StartedAt    *time.Time `json:"started_at,omitempty"`
	CompletedAt  *time.Time `json:"completed_at,omitempty"`
	CreatedAt    time.Time  `json:"created_at"`
	UpdatedAt    time.Time  `json:"updated_at"`
}

OrgImportJob is one archive application. Poll OrganizationService.Import until OrgImportJob.Terminal.

func (*OrgImportJob) Terminal added in v0.3.0

func (j *OrgImportJob) Terminal() bool

Terminal reports whether the job has stopped moving.

type OrgImportParams added in v0.3.0

type OrgImportParams struct {
	// Groups to apply. Empty means every group present in the archive.
	Groups []string `json:"groups,omitempty"`
	// ConflictStrategy is [OrgImportSkip] (the default) or
	// [OrgImportOverwrite].
	ConflictStrategy string `json:"conflict_strategy,omitempty"`
	// Passphrase unseals the archive's secrets bundle. Omit it and the import
	// still runs; mailboxes and integrations just arrive needing a reconnect.
	Passphrase string `json:"-"`
}

OrgImportParams applies an archive.

type OrgImportPreflight added in v0.3.0

type OrgImportPreflight struct {
	Archive *OrgArchiveInfo `json:"archive"`
	// SecretsUnsealed reports whether the passphrase actually opened the
	// secrets bundle, so mailboxes will reconnect on their own.
	SecretsUnsealed bool `json:"secrets_unsealed"`
	// Conflicts is the per-table count of rows already present here.
	Conflicts map[string]int64 `json:"conflicts"`
	// UnknownMembers have no account on this instance; they are imported as
	// pending invitations.
	UnknownMembers []OrgArchiveUser `json:"unknown_members"`
	// SkippedTables are in the archive but unknown here, usually because it
	// came from a newer release.
	SkippedTables []string `json:"skipped_tables"`
	Warnings      []string `json:"warnings"`
}

OrgImportPreflight is what an archive would do, without writing anything.

type OrgRisk added in v0.3.0

type OrgRisk struct {
	// State is one of the OrgRisk* constants.
	State string `json:"state"`
	// Restricted is true for restricted and suspended; volume is reduced.
	Restricted bool `json:"restricted"`
	// Suspended is true when sending is stopped entirely.
	Suspended bool `json:"suspended"`
	// Reason is the sentence a dashboard banner shows, when there is one.
	Reason string `json:"reason,omitempty"`
}

OrgRisk is the workspace's sending posture: whether its volume is capped or stopped, and the plain-language reason. The detector evidence behind it is deliberately not returned.

type OrgTransferCatalog added in v0.3.0

type OrgTransferCatalog struct {
	Groups []OrgDataGroupInfo `json:"groups"`
	// FormatVersion is the archive layout this instance writes and reads.
	FormatVersion int `json:"format_version"`
	// MinPassphrase is the shortest secrets passphrase accepted.
	MinPassphrase int `json:"min_passphrase"`
	// RetentionDays is how long a finished export stays downloadable.
	RetentionDays int `json:"retention_days"`
}

OrgTransferCatalog is what an archive can carry and the rules around it.

type Organization

type Organization struct {
	ID          string    `json:"id"`
	Name        string    `json:"name"`
	Slug        *string   `json:"slug,omitempty"`
	AvatarURL   *string   `json:"avatar_url,omitempty"`
	OwnerUserID string    `json:"owner_user_id"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`

	// DeletionScheduledFor is set while the workspace is pending a hard delete.
	DeletionScheduledAt  *time.Time `json:"deletion_scheduled_at,omitempty"`
	DeletionScheduledFor *time.Time `json:"deletion_scheduled_for,omitempty"`

	// PresenceShowOnline and PresenceShowActivity control team-presence
	// privacy. With PresenceShowOnline false nobody is tracked at all; with
	// PresenceShowActivity false, presence is shown without the
	// viewing-or-editing detail.
	PresenceShowOnline   bool `json:"presence_show_online"`
	PresenceShowActivity bool `json:"presence_show_activity"`

	// ProductDescription, ICPNotes and VoiceProfile are the AI voice profile:
	// organization grounding folded into every AI writing surface.
	ProductDescription string `json:"product_description"`
	ICPNotes           string `json:"icp_notes"`
	VoiceProfile       string `json:"voice_profile"`

	// InboxAgentEnabled opts the workspace into the inbox agent, which drafts
	// a suggested reply on an inbound human reply. It never sends on its own.
	InboxAgentEnabled bool `json:"inbox_agent_enabled"`
	// AssistantSharedHistory makes AI assistant conversations visible to every
	// member with the use-AI permission rather than only their author.
	AssistantSharedHistory bool `json:"assistant_shared_history"`

	Owner *User `json:"owner,omitempty"`
}

Organization is a workspace.

func (*Organization) PendingDeletion

func (o *Organization) PendingDeletion() bool

PendingDeletion reports whether the workspace is scheduled for a hard delete.

type OrganizationCounts

type OrganizationCounts struct {
	TotalCampaigns  int `json:"total_campaigns"`
	ActiveCampaigns int `json:"active_campaigns"`
	TotalContacts   int `json:"total_contacts"`
	TotalMembers    int `json:"total_members"`
	EmailAccounts   int `json:"email_accounts"`
	EmailsSentToday int `json:"emails_sent_today"`
}

OrganizationCounts is current usage against OrganizationLimits.

type OrganizationCreateParams

type OrganizationCreateParams struct {
	Name string `json:"name"`
}

OrganizationCreateParams creates a workspace.

type OrganizationLimits

type OrganizationLimits struct {
	MaxCampaigns       *int `json:"max_campaigns,omitempty"`
	MaxActiveCampaigns *int `json:"max_active_campaigns,omitempty"`
	MaxTeamMembers     *int `json:"max_team_members,omitempty"`
	MaxEmailAccounts   *int `json:"max_email_accounts,omitempty"`
	MaxContacts        *int `json:"max_contacts,omitempty"`
	DailyCampaignLimit *int `json:"daily_campaign_limit,omitempty"`
}

OrganizationLimits are the ceilings the server actually enforces: the plan's, raised by any approved limit increase. A nil field is unlimited.

type OrganizationService

type OrganizationService service

OrganizationService manages the organization (workspace): its settings and AI voice profile, its members, custom roles and invitations, plan limits and sending posture, whole-workspace export and import, and the danger zone.

These routes are session-only. They accept a JWT obtained from AuthService.Login (pass it with WithAccessToken or WithTokenSource) and reject a long-lived API key, because organization governance should never sit behind a static credential.

func (*OrganizationService) AcceptInvitation

func (s *OrganizationService) AcceptInvitation(ctx context.Context, token string, opts ...RequestOption) (*Member, *Response, error)

AcceptInvitation joins the workspace an invite token points at.

func (*OrganizationService) CancelDeletion

func (s *OrganizationService) CancelDeletion(ctx context.Context, reason string, opts ...RequestOption) (*Response, error)

CancelDeletion cancels a pending workspace deletion.

func (*OrganizationService) CancelInvitation

func (s *OrganizationService) CancelInvitation(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

CancelInvitation withdraws a pending invitation.

func (*OrganizationService) CancelLimitRequest

func (s *OrganizationService) CancelLimitRequest(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

CancelLimitRequest withdraws a pending request. Only its submitter may.

func (*OrganizationService) Create

Create provisions a new workspace and returns it.

func (*OrganizationService) CreateExport added in v0.3.0

func (s *OrganizationService) CreateExport(ctx context.Context, params *OrgExportParams, opts ...RequestOption) (*OrgExportJob, *Response, error)

CreateExport starts an archive build in the background and returns the job (202). Poll it with OrganizationService.Export until OrgExportJob.Terminal, then fetch the file with OrganizationService.DownloadExport. Owner-only.

func (*OrganizationService) CreateImport added in v0.3.0

func (s *OrganizationService) CreateImport(ctx context.Context, file *FileUpload, params *OrgImportParams, opts ...RequestOption) (*OrgImportJob, *Response, error)

CreateImport uploads an archive and applies it to the current workspace in the background, returning the job (202). Poll it with OrganizationService.Import. Run OrganizationService.PreflightImport first: an import is irreversible. The upload is multipart (the archive as the file, the options as a JSON form field) and buffered in memory. Owner-only.

func (*OrganizationService) CreateRole

func (s *OrganizationService) CreateRole(ctx context.Context, params *RoleCreateParams, opts ...RequestOption) (*Role, *Response, error)

CreateRole adds a custom role.

func (*OrganizationService) Current

Current retrieves the workspace the session acts on, with its limits and current usage.

func (*OrganizationService) DangerZone

DangerZone returns the workspace's deletion state and the confirmation phrase required to schedule one.

func (*OrganizationService) DeleteAvatar

func (s *OrganizationService) DeleteAvatar(ctx context.Context, opts ...RequestOption) (*Response, error)

DeleteAvatar removes the workspace logo.

func (*OrganizationService) DeleteExport added in v0.3.0

func (s *OrganizationService) DeleteExport(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteExport removes an archive and its stored file. Owner-only.

func (*OrganizationService) DeleteRole

func (s *OrganizationService) DeleteRole(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteRole removes a custom role.

func (*OrganizationService) DownloadExport added in v0.3.0

func (s *OrganizationService) DownloadExport(ctx context.Context, id string, w io.Writer, opts ...RequestOption) (*Response, error)

DownloadExport streams a completed archive (a zip) into w. The response carries the size in Content-Length and the digest in X-Archive-SHA256, so the copy can be verified against OrgExportJob.ArchiveSHA256. A job that has not completed, or whose archive has expired, answers 404. Owner-only, and audited. The download is not retried, so a failure mid-stream leaves w partially written.

func (*OrganizationService) Export added in v0.3.0

Export returns one archive build, for progress polling. Owner-only.

func (*OrganizationService) Exports added in v0.3.0

Exports returns the workspace's recent archive builds, including expired ones. Owner-only.

func (*OrganizationService) Import added in v0.3.0

Import returns one import, for progress polling. Owner-only.

func (*OrganizationService) Imports added in v0.3.0

Imports returns the workspace's recent imports. Owner-only.

func (s *OrganizationService) InvitationLink(ctx context.Context, id string, opts ...RequestOption) (string, *Response, error)

InvitationLink returns the invite token for a pending invitation, so it can be shared out of band. Treat it as a credential: anyone holding it can join.

func (*OrganizationService) Invitations

func (s *OrganizationService) Invitations(ctx context.Context, opts ...RequestOption) ([]Invitation, *Response, error)

Invitations returns the workspace's pending invitations.

func (*OrganizationService) Invite

Invite invites someone to the workspace by email and returns the pending invitation.

func (*OrganizationService) LimitRequests

func (s *OrganizationService) LimitRequests(ctx context.Context, orgID string, opts ...RequestOption) ([]LimitRequest, *Response, error)

LimitRequests returns the workspace's limit-increase requests.

func (*OrganizationService) Limits

Limits returns the ceilings the server enforces for the workspace alongside current usage, the mailbox allowance and attachment storage.

func (*OrganizationService) List

func (s *OrganizationService) List(ctx context.Context, opts ...RequestOption) ([]Member, *Response, error)

List returns the memberships the caller holds across every workspace.

func (*OrganizationService) Members

func (s *OrganizationService) Members(ctx context.Context, opts ...RequestOption) ([]Member, *Response, error)

Members returns the workspace's members.

func (*OrganizationService) MyInvitations

func (s *OrganizationService) MyInvitations(ctx context.Context, opts ...RequestOption) ([]Invitation, *Response, error)

MyInvitations returns the invitations awaiting the caller across every workspace.

func (*OrganizationService) PreflightImport added in v0.3.0

func (s *OrganizationService) PreflightImport(ctx context.Context, file *FileUpload, passphrase string, opts ...RequestOption) (*OrgImportPreflight, *Response, error)

PreflightImport uploads an archive and reports what applying it would do — what it holds, which rows conflict, which members are unknown — without writing anything. Pass the passphrase the archive was exported with to learn whether its secrets unseal; an empty one is fine. The upload is multipart and buffered in memory. Owner-only.

func (*OrganizationService) PreviewInvitation

func (s *OrganizationService) PreviewInvitation(ctx context.Context, token string, opts ...RequestOption) (*InvitationPreview, *Response, error)

PreviewInvitation resolves an invite token to the workspace it points at, before the caller commits to joining. It needs no credentials: the token is the capability. The answer is the safe public view — see InvitationPreview — not the workspace-side Invitation row.

func (*OrganizationService) RemoveMember

func (s *OrganizationService) RemoveMember(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

RemoveMember removes a member from the workspace.

func (*OrganizationService) RequestLimitIncrease

func (s *OrganizationService) RequestLimitIncrease(ctx context.Context, orgID string, params *LimitRequestParams, opts ...RequestOption) (*LimitRequest, *Response, error)

RequestLimitIncrease files a request to raise one plan ceiling.

func (*OrganizationService) Risk added in v0.3.0

Risk returns the workspace's sending posture: whether volume is capped or stopped, and why. Any member may read it; a workspace whose sending is limited should be able to see that it is.

func (*OrganizationService) Roles

func (s *OrganizationService) Roles(ctx context.Context, opts ...RequestOption) ([]Role, *Response, error)

Roles returns the workspace's custom roles.

func (*OrganizationService) ScheduleDeletion

ScheduleDeletion schedules the workspace for a delayed hard delete. It is owner-only, and Confirmation must match DangerZoneStatus.ConfirmationHint — the workspace name.

func (*OrganizationService) Switch

func (s *OrganizationService) Switch(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Switch points the caller's session at another workspace they belong to. Subsequent requests on that session act on the new workspace.

func (*OrganizationService) TransferGroups added in v0.3.0

func (s *OrganizationService) TransferGroups(ctx context.Context, opts ...RequestOption) (*OrgTransferCatalog, *Response, error)

TransferGroups returns the data groups an archive can carry and the rules around one: format version, passphrase floor, export retention.

func (*OrganizationService) TransferOwnership

func (s *OrganizationService) TransferOwnership(ctx context.Context, newOwnerUserID string, opts ...RequestOption) (*Response, error)

TransferOwnership hands the workspace to another member. Only the current owner may call it.

func (*OrganizationService) Update

Update modifies the current workspace.

func (*OrganizationService) UpdateMember

func (s *OrganizationService) UpdateMember(ctx context.Context, id string, params *UpdateMemberParams, opts ...RequestOption) (*Member, *Response, error)

UpdateMember replaces a member's assigned roles.

func (*OrganizationService) UpdateRole

func (s *OrganizationService) UpdateRole(ctx context.Context, id string, params *RoleUpdateParams, opts ...RequestOption) (*Role, *Response, error)

UpdateRole edits a custom role. Changes propagate to every member holding it.

func (*OrganizationService) UploadAvatar

func (s *OrganizationService) UploadAvatar(ctx context.Context, file *FileUpload, opts ...RequestOption) (string, *Response, error)

UploadAvatar sets the workspace logo and returns the URL it is now served from. The image must be a PNG or JPEG, at most 2 MB and 1024 pixels on a side; anything else is refused. The previous logo is deleted.

type OrganizationUpdateParams

type OrganizationUpdateParams struct {
	Name *string `json:"name,omitempty"`
	Slug *string `json:"slug,omitempty"`

	PresenceShowOnline   *bool `json:"presence_show_online,omitempty"`
	PresenceShowActivity *bool `json:"presence_show_activity,omitempty"`

	ProductDescription *string `json:"product_description,omitempty"`
	ICPNotes           *string `json:"icp_notes,omitempty"`
	VoiceProfile       *string `json:"voice_profile,omitempty"`

	InboxAgentEnabled      *bool `json:"inbox_agent_enabled,omitempty"`
	AssistantSharedHistory *bool `json:"assistant_shared_history,omitempty"`
}

OrganizationUpdateParams updates the current workspace. Nil fields are left unchanged; an empty string clears a text field.

type OrganizationWithLimits

type OrganizationWithLimits struct {
	Organization
	Limits *OrganizationLimits `json:"limits,omitempty"`
	Counts *OrganizationCounts `json:"counts,omitempty"`
}

OrganizationWithLimits is an organization together with its plan limits and current usage.

type OutreachService

type OutreachService service

OutreachService reads and writes the organization-wide advanced outreach settings: the bounce pipeline, reply-intent classification, send-time optimization, preflight checks, the in-body unsubscribe line and the rest of the sending policy that campaigns inherit.

A campaign can override any of these for itself; see CampaignService.AdvancedSettings.

func (*OutreachService) Get

Get returns the organization's advanced outreach settings.

func (*OutreachService) Update

func (s *OutreachService) Update(ctx context.Context, settings *OutreachSettings, opts ...RequestOption) (*Response, error)

Update replaces the organization's advanced outreach settings wholesale, so start from OutreachService.Get rather than a zero value. Out-of-range values are clamped rather than refused. The API answers 204 with no body; read the stored result back with Get.

type OutreachSettings

type OutreachSettings struct {
	BouncePipeline       BouncePipelineSettings          `json:"bounce_pipeline"`
	TaskReliability      TaskReliabilitySettings         `json:"task_reliability"`
	ABTesting            ABTestingSettings               `json:"ab_testing"`
	ReplyIntent          ReplyIntentSettings             `json:"reply_intent"`
	SendTimeOptimization SendTimeOptimizationSettings    `json:"send_time_optimization"`
	Preflight            PreflightValidationSettings     `json:"preflight"`
	Dashboard            DeliverabilityDashboardSettings `json:"dashboard"`
	// Unsubscribe is the in-body opt-out line campaigns inherit unless they
	// set their own [Campaign.UnsubscribeMode].
	Unsubscribe UnsubscribeSettings `json:"unsubscribe"`
	// Custom carries forward-compatible keys the server understands but this
	// SDK release does not model.
	Custom map[string]any `json:"custom,omitempty"`
}

OutreachSettings is the full advanced-outreach policy, either for the organization or as a per-campaign override.

type OverallStats

type OverallStats struct {
	TotalEmailsSent int64 `json:"total_emails_sent"`
	TotalOpens      int64 `json:"total_opens"`
	// MachineOpens are opens attributed to a mail-privacy proxy rather than a
	// human, and are excluded from OpenRate.
	MachineOpens int64 `json:"machine_opens"`
	TotalClicks  int64 `json:"total_clicks"`
	// MachineClicks counts steps whose only clicks came from automated
	// fetchers (security gateways walking the links). They are not part of
	// TotalClicks, which only ever counts a person's click.
	MachineClicks   int64   `json:"machine_clicks"`
	TotalReplies    int64   `json:"total_replies"`
	TotalBounces    int64   `json:"total_bounces"`
	OpenRate        float64 `json:"open_rate"`
	ClickRate       float64 `json:"click_rate"`
	ReplyRate       float64 `json:"reply_rate"`
	BounceRate      float64 `json:"bounce_rate"`
	ActiveCampaigns int     `json:"active_campaigns"`
	ActiveAccounts  int     `json:"active_accounts"`
}

OverallStats are the headline counters for the dashboard window.

type Page

type Page[T any] struct {
	// Data holds the items on this page.
	Data []T `json:"data"`
	// Pagination holds the cursor metadata for this page.
	Pagination Pagination `json:"pagination"`
	// contains filtered or unexported fields
}

Page is one page of a list endpoint's results. Beyond Data and Pagination it can fetch subsequent pages (Page.Next) or iterate every item across all pages (Page.All).

func (*Page[T]) All

func (p *Page[T]) All(ctx context.Context) iter.Seq2[T, error]

All returns an iterator over every item across all pages, fetching subsequent pages on demand. Iteration stops at the first error, which is yielded with the zero value of T:

for campaign, err := range page.All(ctx) {
	if err != nil {
		return err
	}
	fmt.Println(campaign.Name)
}
Example

Auto-paging walks every page for you, fetching the next one only when the current one runs out.

package main

import (
	"context"
	"fmt"
	"log"

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

func main() {
	client, _ := warmbly.New(warmbly.WithAPIKey("wmbly_..."))
	ctx := context.Background()

	page, err := client.Emails.List(ctx, &warmbly.EmailListParams{Query: "acme.com"})
	if err != nil {
		log.Fatal(err)
	}
	for mailbox, err := range page.All(ctx) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(mailbox.Email, mailbox.WarmupActive())
	}
}

func (*Page[T]) HasMore

func (p *Page[T]) HasMore() bool

HasMore reports whether a further page is available. It requires both the has_more flag and a non-empty cursor, so a server that reports has_more with an empty/null cursor cannot drive an infinite re-fetch loop.

func (*Page[T]) Next

func (p *Page[T]) Next(ctx context.Context) (*Page[T], error)

Next fetches the following page. It returns ErrNoMorePages when the current page is the last.

func (*Page[T]) NextCursor

func (p *Page[T]) NextCursor() string

NextCursor returns the opaque cursor for the next page, or "" if none.

func (*Page[T]) Response

func (p *Page[T]) Response() *Response

Response returns the HTTP response that produced this page, including rate-limit metadata.

type Pagination

type Pagination struct {
	// Total is the total number of matching items, when the API computes it.
	Total *int64 `json:"total"`
	// NextCursor is the opaque token for the next page, or nil on the last.
	NextCursor *string `json:"next_cursor"`
	// HasMore reports whether further pages exist.
	HasMore bool `json:"has_more"`
}

Pagination is the cursor-based pagination envelope returned by list endpoints. Cursors are opaque: never construct or parse them, just pass NextCursor back to fetch the following page.

type PasskeyCredential

type PasskeyCredential struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	// Provider is the authenticator's origin, for example "icloud" or
	// "android".
	Provider string `json:"provider,omitempty"`
	// CredentialID is the WebAuthn credential identifier.
	CredentialID string `json:"credential_id"`
	// Transports are how the authenticator can be reached, for example
	// "internal" or "hybrid".
	Transports []string `json:"transports"`
	// BackupState reports whether the credential is synced to a cloud keychain
	// rather than bound to one device.
	BackupState bool       `json:"backup_state"`
	CreatedAt   time.Time  `json:"created_at"`
	LastUsedAt  *time.Time `json:"last_used_at,omitempty"`
}

PasskeyCredential is one registered WebAuthn credential.

type PasskeyLoginChallenge added in v0.3.0

type PasskeyLoginChallenge struct {
	// Session is the opaque handle for [AuthService.FinishPasskeyLogin].
	Session string `json:"session"`
	// Options is the raw WebAuthn credential-request options, for the
	// authenticator.
	Options json.RawMessage `json:"options"`
}

PasskeyLoginChallenge is the start of a passkey sign-in: the WebAuthn request options for the authenticator and the handle to return with its assertion.

type PendingTool

type PendingTool struct {
	MessageID  string `json:"message_id"`
	ToolCallID string `json:"tool_call_id"`
	ToolName   string `json:"tool_name"`
	// Risk is how consequential the call is, for example "send".
	Risk string `json:"risk"`
	// Args is the exact payload the tool would run with, and ArgsSummary a
	// human-readable rendering of it.
	Args        json.RawMessage `json:"args"`
	ArgsSummary string          `json:"args_summary,omitempty"`
}

PendingTool is a tool call paused for human approval.

type Permission

type Permission struct {
	// Name is the stable identifier, for example "READ_CAMPAIGNS".
	Name  string `json:"name"`
	Value uint64 `json:"value"`
	// Description is human-readable copy for a permission picker.
	Description string `json:"description"`
	// Category groups the bit as "read", "write", "bulk" or "special".
	Category string `json:"category"`
}

Permission describes one scope bit the API advertises.

type PermissionCatalog

type PermissionCatalog struct {
	Permissions []Permission      `json:"permissions"`
	Presets     PermissionPresets `json:"presets"`
}

PermissionCatalog is the full set of API scopes plus the server's preset masks. Prefer the presets over hard-coding a mask when you want "everything": the server's value stays current as scopes are added.

type PermissionPresets

type PermissionPresets struct {
	ReadOnly   uint64 `json:"read_only"`
	FullAccess uint64 `json:"full_access"`
}

PermissionPresets are the server-side preset masks.

type Pipeline

type Pipeline struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	Name           string `json:"name"`
	Position       int    `json:"position"`
	// Stages is populated on reads that hydrate the pipeline.
	Stages    []PipelineStage `json:"stages,omitempty"`
	CreatedAt time.Time       `json:"created_at"`
	UpdatedAt time.Time       `json:"updated_at"`
}

Pipeline is a named deal pipeline: an ordered set of stages deals move through.

type PipelineCreateParams

type PipelineCreateParams struct {
	Name   string              `json:"name"`
	Stages []StageCreateParams `json:"stages,omitempty"`
}

PipelineCreateParams creates a pipeline, optionally with its initial stages.

type PipelineStage

type PipelineStage struct {
	ID         string `json:"id"`
	PipelineID string `json:"pipeline_id"`
	Name       string `json:"name"`
	Color      string `json:"color"`
	Position   int    `json:"position"`
	// DealCount is set on reads that count the stage's deals.
	DealCount int       `json:"deal_count,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

PipelineStage is one column of a pipeline.

type PipelineUpdateParams

type PipelineUpdateParams struct {
	Name *string `json:"name,omitempty"`
}

PipelineUpdateParams renames a pipeline.

type Plan

type Plan struct {
	ID   string  `json:"id"`
	Name *string `json:"name,omitempty"`

	MaxContacts  uint `json:"max_contacts"`
	DailyEmails  uint `json:"daily_emails"`
	AIGeneration bool `json:"ai_generation"`
	AccountLimit uint `json:"account_limit"`

	Price           float32 `json:"price"`
	DiscountedPrice float32 `json:"discounted_price"`
	// Duration is [DurationMonth] or [DurationYear].
	Duration string `json:"duration"`
	// Savings is the percentage saved against the monthly price.
	Savings uint8 `json:"savings"`
	// Public reports whether the plan is offered on the pricing page.
	Public bool `json:"public"`

	StripePriceID       *string `json:"stripe_price_id,omitempty"`
	StripePriceIDYearly *string `json:"stripe_price_id_yearly,omitempty"`
	StripeProductID     *string `json:"stripe_product_id,omitempty"`

	DedicatedWorkers   int  `json:"dedicated_workers"`
	DailyCampaignLimit *int `json:"daily_campaign_limit,omitempty"`

	MaxCampaigns       *int `json:"max_campaigns,omitempty"`
	MaxActiveCampaigns *int `json:"max_active_campaigns,omitempty"`
	MaxTeamMembers     *int `json:"max_team_members,omitempty"`
	MaxEmailAccounts   *int `json:"max_email_accounts,omitempty"`

	// MonthlyCredits is the AI credit grant included each month.
	MonthlyCredits int `json:"monthly_credits"`
	// ReferralRewardPercent is the share of this plan's month-equivalent
	// price a referrer earns when an invitee converts to it; 100 is a full
	// month.
	ReferralRewardPercent int `json:"referral_reward_percent"`

	CreatedAt time.Time `json:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

Plan is one purchasable plan and the ceilings it carries.

type PlanChangePreview added in v0.3.0

type PlanChangePreview struct {
	CurrentPlan *Plan `json:"current_plan"`
	NewPlan     *Plan `json:"new_plan"`
	// ProrationAmount is the credit or charge for the unused part of the
	// current period, and AmountDue what the next invoice comes to. Both are
	// in the smallest unit of Currency, and either can be negative.
	ProrationAmount int64 `json:"proration_amount"`
	AmountDue       int64 `json:"amount_due"`
	// NextBillingDate is when that invoice falls due.
	NextBillingDate time.Time `json:"next_billing_date"`
	Currency        string    `json:"currency"`
}

PlanChangePreview is what a plan change would cost, computed by the payment provider without making the change.

type PoolLinkCode added in v0.3.0

type PoolLinkCode struct {
	ID string `json:"id"`
	// UserCode is the code the operator typed in.
	UserCode string `json:"user_code"`
	// InstanceName, InstanceURL and InstanceVersion are what the instance
	// claimed about itself in [PoolLinkStartParams]; nothing here is verified.
	InstanceName    string `json:"instance_name"`
	InstanceURL     string `json:"instance_url"`
	InstanceVersion string `json:"instance_version"`
	// Status is one of the PoolLinkCode* constants.
	Status string `json:"status"`
	// OrganizationID and InstanceID are set once the code has been approved.
	OrganizationID *string `json:"organization_id,omitempty"`
	InstanceID     *string `json:"instance_id,omitempty"`
	// ExpiresAt is when an unsettled code stops being approvable.
	ExpiresAt time.Time `json:"expires_at"`
	CreatedAt time.Time `json:"created_at"`
}

PoolLinkCode describes a pending handshake to the member about to settle it.

type PoolLinkCodeGrant added in v0.3.0

type PoolLinkCodeGrant struct {
	// DeviceCode is the secret half: keep it on the instance and send it only
	// to [PoolLinkService.Poll].
	DeviceCode string `json:"device_code"`
	// UserCode is the short code the operator types in at VerificationURL.
	UserCode string `json:"user_code"`
	// VerificationURL is the page where a signed-in member approves the code.
	// It already carries the user code as a query parameter.
	VerificationURL string `json:"verification_url"`
	// ExpiresIn is how long the code stays approvable, in seconds (15 minutes
	// at the time of writing).
	ExpiresIn int `json:"expires_in"`
	// Interval is the minimum number of seconds between polls.
	Interval int `json:"interval"`
}

PoolLinkCodeGrant is the device-code grant that opens a handshake.

type PoolLinkInstance added in v0.3.0

type PoolLinkInstance struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	Name           string `json:"name"`
	URL            string `json:"url"`
	// Version is the build the instance most recently reported.
	Version string `json:"version"`
	// CreatedBy is the member who approved the link, when still known.
	CreatedBy *string   `json:"created_by,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	// LastSeenAt is the instance's most recent authenticated call.
	LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
	// RevokedAt is set once the link has been ended from either side.
	RevokedAt *time.Time `json:"revoked_at,omitempty"`
	// MailboxCount is how many of the instance's mailboxes are enrolled in the
	// pool. It is filled by [PoolLinkService.ListInstances] only and is zero
	// on the instance returned from [PoolLinkService.ApproveCode].
	MailboxCount int `json:"mailbox_count"`
}

PoolLinkInstance is a linked self-hosted instance as the workspace sees it.

type PoolLinkInstanceInfo added in v0.3.0

type PoolLinkInstanceInfo struct {
	Instance     PoolLinkInstance     `json:"instance"`
	Organization PoolLinkOrganization `json:"organization"`
	Plan         PoolLinkPlan         `json:"plan"`
}

PoolLinkInstanceInfo is the cloud's status document for one linked instance, as the instance itself sees it. The self-hosted dashboard surfaces it through CloudLinkStatus.Info.

type PoolLinkInstanceList added in v0.3.0

type PoolLinkInstanceList struct {
	Data []PoolLinkInstance `json:"data"`
	Plan PoolLinkPlan       `json:"plan"`
}

PoolLinkInstanceList is the answer to PoolLinkService.ListInstances: the linked instances plus the allowance they share.

type PoolLinkMailboxState added in v0.3.0

type PoolLinkMailboxState struct {
	// RemoteID is the mailbox's ID on the self-hosted instance;
	// EmailAccountID is its ID in the cloud workspace.
	RemoteID       string `json:"remote_id"`
	EmailAccountID string `json:"email_account_id"`
	Email          string `json:"email"`
	Name           string `json:"name"`
	// Provider is [ProviderGmail], [ProviderOutlook] or [ProviderSMTPIMAP].
	Provider string `json:"provider"`
	// Status is the cloud mailbox's connection state, for example
	// [MailboxStatusActive].
	Status     string    `json:"status"`
	EnrolledAt time.Time `json:"enrolled_at"`
	// Managed is true when the cloud holds the only credential (a Google or
	// Microsoft sign-in on Warmbly's own OAuth apps) and the instance sends
	// with brokered short-lived tokens.
	Managed bool                  `json:"managed"`
	Warmup  *PoolLinkWarmupStatus `json:"warmup,omitempty"`
	Health  *PoolLinkWarmupHealth `json:"health,omitempty"`
	// SentToday, Sent7d, Replied7d and SpamPlaced7d are warmup counters.
	SentToday    int `json:"sent_today"`
	Sent7d       int `json:"sent_7d"`
	Replied7d    int `json:"replied_7d"`
	SpamPlaced7d int `json:"spam_placed_7d"`
	// Errors are the cloud mailbox's open account errors.
	Errors []AccountError `json:"errors,omitempty"`
	// AuthState is the cloud's view of the mailbox credential.
	AuthState string                 `json:"auth_state"`
	Settings  PoolLinkWarmupSettings `json:"settings"`
}

PoolLinkMailboxState is the cloud's per-mailbox view of an enrolled mailbox, shown in both dashboards.

type PoolLinkOrganization added in v0.3.0

type PoolLinkOrganization struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

PoolLinkOrganization is the cloud workspace a link belongs to.

type PoolLinkPlan added in v0.3.0

type PoolLinkPlan struct {
	// Tier is [PoolLinkTierFree] or [PoolLinkTierPaid].
	Tier string `json:"tier"`
	// MailboxLimit is the cap on enrolled mailboxes across every linked
	// instance, or nil when unlimited.
	MailboxLimit *int `json:"mailbox_limit"`
	// Enrolled is how many linked mailboxes currently count against the cap.
	Enrolled int `json:"enrolled"`
	// PriceUSD is the monthly price of the paid tier, for an upgrade prompt.
	PriceUSD int `json:"price_usd"`
	// UpgradeURL is where to send the operator to upgrade; empty when billing
	// is off or the paid tier has no price.
	UpgradeURL string `json:"upgrade_url,omitempty"`
	// WarmupEntitled is false when the cloud workspace itself is not allowed
	// to warm, in which case linked mailboxes will not warm either.
	WarmupEntitled bool `json:"warmup_entitled"`
}

PoolLinkPlan is the pool allowance the cloud computes for a workspace's linked instances.

type PoolLinkPollResult added in v0.3.0

type PoolLinkPollResult struct {
	// Status is [PoolLinkCodePending] or [PoolLinkCodeApproved]; a denied or
	// spent code is reported as an error, not a status.
	Status string `json:"status"`
	// InstanceID is set once approved.
	InstanceID *string `json:"instance_id,omitempty"`
	// InstanceToken is the instance's long-lived credential for
	// /pool-link/instance/*. It is delivered exactly once: store it
	// immediately, because the next poll fails with "pool_link_code_used".
	InstanceToken string `json:"instance_token,omitempty"`
	// Organization is the workspace the instance was linked to, once approved.
	Organization *PoolLinkOrganization `json:"organization,omitempty"`
}

PoolLinkPollResult is one answer to PoolLinkService.Poll.

type PoolLinkService added in v0.3.0

type PoolLinkService service

PoolLinkService is the hosted (cloud) side of the self-hosted warmup pool link: a self-hosted Warmbly instance asks to warm its mailboxes in this workspace's pool, a member approves it, and the workspace then lists and unlinks the instances it has admitted.

Linking is a device-code handshake. The instance calls PoolLinkService.StartCode before it holds any credential and shows the returned user code to its operator; that operator signs in to Warmbly Cloud, reviews the code with PoolLinkService.DescribeCode and settles it with PoolLinkService.ApproveCode or PoolLinkService.DenyCode; meanwhile the instance loops on PoolLinkService.Poll (or PoolLinkService.WaitForApproval) until the code is approved and its instance token is delivered, once.

Only StartCode and Poll are public: they need no credential at all and share the per-IP sign-in rate budget, so a runaway poll loop can lock the address out of the browser login for the rest of the window. Every other route is session-only: it needs a token from AuthService.Login, never an API key, because approving a code mints a credential for a third party. Listing and revoking instances additionally need the manage-settings organization permission.

The instance's own machine-to-machine surface, /pool-link/instance/* (status, enrollment, brokered access tokens), accepts only the instance token minted by the handshake and is deliberately not modeled here; the self-hosted dashboard reaches it through CloudLinkService.

On a deployment that has the pool link switched off every route answers 501 Not Implemented.

func (*PoolLinkService) ApproveCode added in v0.3.0

func (s *PoolLinkService) ApproveCode(ctx context.Context, userCode, organizationID string, opts ...RequestOption) (*PoolLinkInstance, *Response, error)

ApproveCode admits the instance behind userCode into the workspace organizationID and returns the new linked instance. The instance collects its token on its next poll.

organizationID is the workspace to link to, which need not be the session's active one; pass "" to fall back to the session's workspace. The caller needs the manage-settings permission in that workspace. Session-only; the call is recorded in the workspace audit log.

A code that is no longer pending fails with "pool_link_code_used".

func (*PoolLinkService) DenyCode added in v0.3.0

func (s *PoolLinkService) DenyCode(ctx context.Context, userCode string, opts ...RequestOption) (*Response, error)

DenyCode declines the handshake behind userCode. The instance's next poll fails with "pool_link_denied" and the code cannot be approved afterwards. Session-only.

func (*PoolLinkService) DescribeCode added in v0.3.0

func (s *PoolLinkService) DescribeCode(ctx context.Context, userCode string, opts ...RequestOption) (*PoolLinkCode, *Response, error)

DescribeCode shows the signed-in member what they are about to link: the instance's self-reported name, URL and version, and whether the code is still pending. Session-only.

An unknown or expired code fails with "pool_link_code_not_found".

func (*PoolLinkService) ListInstances added in v0.3.0

func (s *PoolLinkService) ListInstances(ctx context.Context, opts ...RequestOption) (*PoolLinkInstanceList, *Response, error)

ListInstances lists the instances linked to the session's workspace, together with the pool allowance they share. Session-only; needs the manage-settings permission.

func (*PoolLinkService) Poll added in v0.3.0

func (s *PoolLinkService) Poll(ctx context.Context, deviceCode string, opts ...RequestOption) (*PoolLinkPollResult, *Response, error)

Poll asks once whether the code behind deviceCode has been approved. While nobody has settled it the result is PoolLinkCodePending; once a member approves it the result is PoolLinkCodeApproved with the instance token, which is delivered on this one call only.

A denied code fails with an *Error whose Code is "pool_link_denied"; an already collected one with "pool_link_code_used"; an unknown or expired one with "pool_link_code_not_found". Wait at least PoolLinkCodeGrant.Interval seconds between calls: the route is public and draws on the per-IP sign-in budget.

func (*PoolLinkService) RevokeInstance added in v0.3.0

func (s *PoolLinkService) RevokeInstance(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

RevokeInstance unlinks an instance: its token stops working, every mailbox it enrolled leaves the pool and their credentials are deleted on the cloud. The instance's next call fails with "pool_link_revoked" and its local warmup takes over. Session-only; needs the manage-settings permission and is recorded in the workspace audit log.

func (*PoolLinkService) StartCode added in v0.3.0

StartCode opens a device-code handshake on behalf of a self-hosted instance that holds no credential yet. Show PoolLinkCodeGrant.UserCode and PoolLinkCodeGrant.VerificationURL to the operator, keep PoolLinkCodeGrant.DeviceCode private, and follow up with PoolLinkService.WaitForApproval.

Public and per-IP rate limited; the client's credential, if any, is sent but ignored.

func (*PoolLinkService) WaitForApproval added in v0.3.0

func (s *PoolLinkService) WaitForApproval(ctx context.Context, grant *PoolLinkCodeGrant, opts ...RequestOption) (*PoolLinkPollResult, error)

WaitForApproval polls grant every PoolLinkCodeGrant.Interval seconds (never faster than once a second) until the code is approved, the server reports a terminal error, or ctx is done. On success the result carries the one-time instance token.

The server retires an unapproved code after PoolLinkCodeGrant.ExpiresIn seconds, after which the loop ends with a "pool_link_code_not_found" error; bound ctx yourself to give up sooner.

type PoolLinkStartParams added in v0.3.0

type PoolLinkStartParams struct {
	// InstanceName is the label the approval page shows, typically the host
	// name or the operator's INSTANCE_NAME.
	InstanceName string `json:"instance_name"`
	// InstanceURL is the instance's public base URL.
	InstanceURL string `json:"instance_url"`
	// InstanceVersion is the instance's build, for support and compatibility.
	InstanceVersion string `json:"instance_version"`
}

PoolLinkStartParams identifies the instance that wants to link, as shown to the approving member.

type PoolLinkWarmupHealth added in v0.3.0

type PoolLinkWarmupHealth struct {
	// State is one of the Band* constants: [BandHealthy], [BandWatch],
	// [BandThrottled], [BandQuarantined] or [BandBlocked].
	State string  `json:"state"`
	Score float64 `json:"score"`
	// Reason explains a state other than healthy.
	Reason    string `json:"reason,omitempty"`
	SpamScore int    `json:"spam_score"`
	// BlockedUntil is when a quarantine or block lifts on its own.
	BlockedUntil *time.Time `json:"blocked_until,omitempty"`
	EvaluatedAt  *time.Time `json:"evaluated_at,omitempty"`
}

PoolLinkWarmupHealth is the pool's reputation verdict on an enrolled mailbox.

type PoolLinkWarmupRampHold added in v0.3.0

type PoolLinkWarmupRampHold struct {
	// Placements and Sends cover the last 48 hours.
	Placements int  `json:"placements"`
	Sends      int  `json:"sends"`
	VolumeCut  bool `json:"volume_cut"`
	// ResumesAt is when the ramp climbs again if nothing else lands.
	ResumesAt time.Time `json:"resumes_at"`
}

PoolLinkWarmupRampHold explains a frozen ramp. It is present for the whole freeze; VolumeCut says whether today's volume is also reduced, which lasts a shorter window.

type PoolLinkWarmupSettings added in v0.3.0

type PoolLinkWarmupSettings struct {
	// Base is the starting daily volume, Max the ceiling and Increase the
	// daily step between them.
	Base     int `json:"base"`
	Max      int `json:"max"`
	Increase int `json:"increase"`
	// ReplyRate is the percentage of warmup mail that gets a reply.
	ReplyRate int `json:"reply_rate"`
	// StartTime and EndTime bound the sending window, as "HH:MM".
	StartTime string `json:"start_time"`
	EndTime   string `json:"end_time"`
	// Days is a bitmask of weekdays the mailbox warms on.
	Days     int    `json:"days"`
	Timezone string `json:"timezone"`
}

PoolLinkWarmupSettings is the ramp an enrolled mailbox warms on. It is copied from the mailbox's own settings on the instance at enrollment time; zero values mean the cloud's defaults.

type PoolLinkWarmupStatus added in v0.3.0

type PoolLinkWarmupStatus struct {
	Enabled  bool       `json:"enabled"`
	Paused   bool       `json:"paused"`
	PausedAt *time.Time `json:"paused_at,omitempty"`
	// StartedAt is when the ramp began.
	StartedAt time.Time `json:"started_at"`
	// CurrentVolume is today's sends so far against TargetVolume, today's
	// ramp target, which climbs toward MaxVolume.
	CurrentVolume int `json:"current_volume"`
	TargetVolume  int `json:"target_volume"`
	MaxVolume     int `json:"max_volume"`
	ReplyRate     int `json:"reply_rate"`
	DaysActive    int `json:"days_active"`
	// RampHold explains a ramp that is not climbing, so a target below the
	// plain ramp is never an unexplained drop.
	RampHold *PoolLinkWarmupRampHold `json:"ramp_hold,omitempty"`
}

PoolLinkWarmupStatus is the live ramp state of an enrolled mailbox.

type PoolLinkWorkspaceMailbox added in v0.3.0

type PoolLinkWorkspaceMailbox struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name"`
	// Provider is [ProviderGmail] or [ProviderOutlook].
	Provider string `json:"provider"`
	Status   string `json:"status"`
}

PoolLinkWorkspaceMailbox is a Google or Microsoft mailbox connected directly on the cloud workspace that a linked instance may adopt.

type PreflightCheck

type PreflightCheck struct {
	Key    string `json:"key"`
	Passed bool   `json:"passed"`
	// Severity is how much a failure matters, for example "error" or "warning".
	Severity    string `json:"severity"`
	Message     string `json:"message"`
	Remediation string `json:"remediation,omitempty"`
}

PreflightCheck is a single pre-launch check.

type PreflightResult

type PreflightResult struct {
	ID             string `json:"id,omitempty"`
	OrganizationID string `json:"organization_id,omitempty"`
	CampaignID     string `json:"campaign_id"`
	// Passed is false when any blocking check failed.
	Passed bool `json:"passed"`
	// Score is a 0-100 readiness score.
	Score           int              `json:"score"`
	Checks          []PreflightCheck `json:"checks"`
	Recommendations []string         `json:"recommendations,omitempty"`
	CreatedAt       time.Time        `json:"created_at,omitempty"`
}

PreflightResult is the outcome of the pre-launch checks for a campaign.

type PreflightValidationSettings

type PreflightValidationSettings struct {
	Enabled                  bool `json:"enabled"`
	CheckTrackingDomain      bool `json:"check_tracking_domain"`
	CheckUnsubscribeHeader   bool `json:"check_unsubscribe_header"`
	CheckABVariantConfigured bool `json:"check_ab_variant_configured"`
	CheckDailyLimit          bool `json:"check_daily_limit"`
	CheckScheduleWindow      bool `json:"check_schedule_window"`
	// CheckContentScore scores each step's copy for spam signals, at preflight
	// and again per send against the rendered text. It is advisory: it warns
	// and never blocks a send. On by default.
	CheckContentScore bool `json:"check_content_score"`
	// MinContentScore is the 1-100 floor below which copy is flagged (default
	// 60). Values outside the range are clamped server-side.
	MinContentScore int `json:"min_content_score"`
}

PreflightValidationSettings selects the checks run before a campaign starts.

type ProfileUpdateParams

type ProfileUpdateParams struct {
	FirstName string `json:"first_name,omitempty"`
	LastName  string `json:"last_name,omitempty"`
}

ProfileUpdateParams updates the caller's own name.

type ProviderPlacement added in v0.3.0

type ProviderPlacement struct {
	Provider   string  `json:"provider"`
	Samples    int64   `json:"samples"`
	Inbox      int64   `json:"inbox"`
	Promotions int64   `json:"promotions"`
	Spam       int64   `json:"spam"`
	Other      int64   `json:"other"`
	InboxRate  float64 `json:"inbox_rate"`
	SpamRate   float64 `json:"spam_rate"`
}

ProviderPlacement is one recipient provider's seed placement rollup: where the seed messages landed.

type PushContactResult

type PushContactResult struct {
	ContactID string `json:"contact_id"`
	Email     string `json:"email,omitempty"`
	OK        bool   `json:"ok"`
	Error     string `json:"error,omitempty"`
}

PushContactResult is one contact's outcome in a push.

type PushResult

type PushResult struct {
	Provider string              `json:"provider"`
	Pushed   int                 `json:"pushed"`
	Failed   int                 `json:"failed"`
	Results  []PushContactResult `json:"results"`
}

PushResult reports the outcome of pushing contacts to a provider.

type RateLimit

type RateLimit struct {
	// Limit is the ceiling of requests permitted in the current window.
	Limit int
	// Remaining is the number of requests left in the current window.
	Remaining int
	// Policy is the raw policy string, e.g. "60;w=60".
	Policy string
	// RetryAfter is how long to wait before retrying, when provided.
	RetryAfter time.Duration
}

RateLimit is the per-key rate-limit state reported on each response.

type RealtimeInfo

type RealtimeInfo struct {
	// WebsocketURL is the endpoint to dial.
	WebsocketURL string `json:"websocket_url"`
	// Topics are the channels this caller may subscribe to.
	Topics []string `json:"topics"`
}

RealtimeInfo tells a client where the realtime gateway lives.

type RealtimeRateLimits added in v0.3.0

type RealtimeRateLimits struct {
	LimitWSMessagePM int `json:"limit_ws_message_pm"`
	LimitWSJoinPM    int `json:"limit_ws_join_pm"`
	LimitWSEventPM   int `json:"limit_ws_event_pm"`
	MaxConnections   int `json:"max_connections"`
}

RealtimeRateLimits are the plan's websocket ceilings: messages, channel joins and events per minute, and concurrent connections.

type ReferralAttribution

type ReferralAttribution struct {
	ID             string  `json:"id"`
	ReferralCodeID *string `json:"referral_code_id,omitempty"`
	ReferrerUserID string  `json:"referrer_user_id"`
	ReferrerOrgID  string  `json:"referrer_org_id"`
	InviteeOrgID   string  `json:"invitee_org_id"`
	InviteeUserID  *string `json:"invitee_user_id,omitempty"`
	// Status is one of the ReferralStatus* constants.
	Status         string `json:"status"`
	RewardCents    int64  `json:"reward_cents"`
	RewardCurrency string `json:"reward_currency"`

	QualifiedAt *time.Time `json:"qualified_at,omitempty"`
	RewardedAt  *time.Time `json:"rewarded_at,omitempty"`
	VoidedAt    *time.Time `json:"voided_at,omitempty"`
	VoidReason  *string    `json:"void_reason,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

ReferralAttribution is one referred workspace and where its reward stands.

type ReferralCode added in v0.3.0

type ReferralCode struct {
	ID          string `json:"id"`
	OwnerUserID string `json:"owner_user_id"`
	OwnerOrgID  string `json:"owner_org_id"`
	// Code is the token that goes in a share link's ?ref= parameter and in
	// [LoginParams.ReferralCode].
	Code string `json:"code"`
	// DiscountCodeID is the promotion an invitee redeems by signing up with
	// the code, when the deployment attaches one.
	DiscountCodeID *string   `json:"discount_code_id,omitempty"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

ReferralCode is the workspace's own referral code, as returned by BillingService.EnsureReferralCode.

type ReferralEarning

type ReferralEarning struct {
	ID    string `json:"id"`
	OrgID string `json:"org_id"`
	// AttributionID is the referral the movement settles, when there is one.
	AttributionID *string `json:"attribution_id,omitempty"`
	// AmountCents is positive for a reward and negative for a clawback.
	AmountCents int64  `json:"amount_cents"`
	Currency    string `json:"currency"`
	// Reason names what moved the balance.
	Reason string `json:"reason"`
	// BalanceAfterCents is the running balance once this movement applied.
	BalanceAfterCents int64 `json:"balance_after_cents"`
	// StripeCustomerBalanceTxnID links the movement to the provider-side
	// customer balance transaction it produced.
	StripeCustomerBalanceTxnID *string   `json:"stripe_customer_balance_txn_id,omitempty"`
	CreatedAt                  time.Time `json:"created_at"`
}

ReferralEarning is one movement in the referral credit ledger.

type ReferralSummary

type ReferralSummary struct {
	Code     string `json:"code"`
	ShareURL string `json:"share_url"`
	Currency string `json:"currency"`
	// InviteePercentOff and InviteeMonths are what someone signing up through
	// the link receives.
	InviteePercentOff int `json:"invitee_percent_off"`
	InviteeMonths     int `json:"invitee_months"`

	// BalanceCents is the credit available now; LifetimeEarnedCents is the
	// running total.
	BalanceCents        int64 `json:"balance_cents"`
	LifetimeEarnedCents int64 `json:"lifetime_earned_cents"`

	TotalReferred int `json:"total_referred"`
	Pending       int `json:"pending"`
	Qualified     int `json:"qualified"`
	Rewarded      int `json:"rewarded"`
}

ReferralSummary is the workspace's referral position.

type ReplyIntentSettings

type ReplyIntentSettings struct {
	Enabled                 bool     `json:"enabled"`
	PositiveKeywords        []string `json:"positive_keywords"`
	NegativeKeywords        []string `json:"negative_keywords"`
	OutOfOfficeKeywords     []string `json:"out_of_office_keywords"`
	QuestionKeywords        []string `json:"question_keywords"`
	AutoCreateCRMTask       bool     `json:"auto_create_crm_task"`
	AutoPauseOnNegative     bool     `json:"auto_pause_on_negative"`
	AutoSuppressOnUnsubWord bool     `json:"auto_suppress_on_unsubscribe_keyword"`
}

ReplyIntentSettings configures keyword-based classification of inbound replies and the actions taken on each class.

type RequestOption

type RequestOption func(*requestConfig)

RequestOption customizes a single API call. Every service method accepts a variadic list of them.

func WithIdempotencyKey

func WithIdempotencyKey(key string) RequestOption

WithIdempotencyKey attaches an Idempotency-Key to a mutating request. Retrying the same key with the same method, path and body replays the original response instead of performing the operation twice. The key must be 1-255 visible ASCII characters; a UUID is a good choice.

_, _, err := client.Emails.Send(ctx, id, params, warmbly.WithIdempotencyKey(key))
Example

An idempotency key makes a retry safe on anything that sends mail or spends money: repeating the key replays the original response instead of acting twice.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

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

func main() {
	client, _ := warmbly.New(warmbly.WithAPIKey("wmbly_..."))

	result, resp, err := client.Emails.Send(context.Background(), "mailbox_id", &warmbly.SendEmailParams{
		To:        []string{"prospect@example.com"},
		Subject:   "Hello",
		BodyPlain: "Hi there.",
	}, warmbly.WithIdempotencyKey("order-4171-welcome"))
	if err != nil {
		log.Fatal(err)
	}
	if resp.IdempotentReplayed {
		fmt.Println("already sent; replayed the original response")
	}
	fmt.Println("queued", result.TaskID, "for", result.ScheduledAt.Format(time.RFC3339))
}

func WithQueryParam

func WithQueryParam(key, value string) RequestOption

WithQueryParam sets a query parameter on a single request. Use it to reach filters newer than this SDK release without waiting for a typed field.

func WithRequestHeader

func WithRequestHeader(key, value string) RequestOption

WithRequestHeader sets a header on a single request, overriding any client default of the same name.

type ResearchArtifact

type ResearchArtifact struct {
	What  string `json:"what"`
	Where string `json:"where"`
	When  string `json:"when,omitempty"`
	URL   string `json:"url"`
}

ResearchArtifact is something public the contact produced.

type ResearchCompany

type ResearchCompany struct {
	Summary            string   `json:"summary,omitempty"`
	Industry           string   `json:"industry,omitempty"`
	SizeEstimate       string   `json:"size_estimate,omitempty"`
	SellsTo            string   `json:"sells_to,omitempty"`
	TechOrStackSignals []string `json:"tech_or_stack_signals,omitempty"`
}

ResearchCompany is what a run learned about the contact's company.

type ResearchHook

type ResearchHook struct {
	BasedOn     string `json:"based_on"`
	WhyRelevant string `json:"why_relevant"`
	OpenerLine  string `json:"opener_line"`
}

ResearchHook is a suggested opener grounded in a signal.

type ResearchPerson

type ResearchPerson struct {
	RoleConfirmed   bool               `json:"role_confirmed"`
	Title           string             `json:"title,omitempty"`
	PublicArtifacts []ResearchArtifact `json:"public_artifacts,omitempty"`
}

ResearchPerson is what a run learned about the contact themselves.

type ResearchResult

type ResearchResult struct {
	Company *ResearchCompany `json:"company,omitempty"`
	Person  *ResearchPerson  `json:"person,omitempty"`
	Signals []ResearchSignal `json:"signals,omitempty"`
	Hooks   []ResearchHook   `json:"hooks,omitempty"`
	// CustomFieldUpdates are contact custom fields the run proposes.
	CustomFieldUpdates map[string]string `json:"custom_field_updates,omitempty"`
	ResearchNotes      string            `json:"research_notes,omitempty"`
	NothingFound       bool              `json:"nothing_found"`
}

ResearchResult is what a run found. Every signal and artifact carries a source URL; the server rejects anything uncited.

type ResearchRun

type ResearchRun struct {
	ID          string  `json:"id"`
	OrgID       string  `json:"org_id"`
	ContactID   string  `json:"contact_id"`
	RequestedBy *string `json:"requested_by,omitempty"`
	// Status is one of the Research* constants.
	Status         string         `json:"status"`
	Objective      string         `json:"objective"`
	Result         ResearchResult `json:"result"`
	Error          string         `json:"error,omitempty"`
	CreditsCharged int            `json:"credits_charged"`
	ModelUsed      string         `json:"model_used"`
	TokensUsed     int            `json:"tokens_used"`
	CreatedAt      time.Time      `json:"created_at"`
	UpdatedAt      time.Time      `json:"updated_at"`
}

ResearchRun is one AI research attempt against a contact. Runs spend AI credits.

type ResearchSignal

type ResearchSignal struct {
	Type       string `json:"type"`
	Fact       string `json:"fact"`
	When       string `json:"when,omitempty"`
	URL        string `json:"url"`
	Confidence string `json:"confidence"`
}

ResearchSignal is a cited fact. Confidence is "high", "medium" or "low".

type Response

type Response struct {
	*http.Response

	// RateLimit holds the parsed X-RateLimit-* headers, when present.
	RateLimit RateLimit
	// RequestID is the server-assigned request identifier (X-Request-ID).
	RequestID string
	// APIVersion is the API surface that served the request (for example
	// "v1"), taken from the API-Version header.
	APIVersion string
	// Deprecation is the raw Deprecation header, set when the endpoint is on
	// its way out.
	Deprecation string
	// Sunset is the raw Sunset header: the date after which a deprecated
	// endpoint stops responding.
	Sunset string
	// Warning is the raw Warning header carrying migration advice.
	Warning string
	// IdempotentReplayed reports whether the server replayed a stored response
	// for a repeated Idempotency-Key rather than acting again.
	IdempotentReplayed bool
}

Response wraps the underlying *http.Response with parsed Warmbly metadata. The body has already been consumed and closed by the time a Response is returned.

func (*Response) Deprecated

func (r *Response) Deprecated() bool

Deprecated reports whether the server marked this endpoint as deprecated.

type Role

type Role struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	Name           string `json:"name"`
	Description    string `json:"description"`
	Color          string `json:"color"`
	// Permissions is the role's grant, an OR of the OrgPerm* bits.
	Permissions uint16    `json:"permissions"`
	MemberCount int       `json:"member_count"`
	CreatedAt   time.Time `json:"created_at"`
	UpdatedAt   time.Time `json:"updated_at"`
}

Role is an organization-scoped custom role: a named permission set members are assigned to. Editing a role writes through to every member holding it.

type RoleCreateParams

type RoleCreateParams struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Color       string `json:"color,omitempty"`
	Permissions uint16 `json:"permissions"`
}

RoleCreateParams creates a custom role. The name "owner" is reserved.

type RoleUpdateParams

type RoleUpdateParams struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	Color       *string `json:"color,omitempty"`
	Permissions *uint16 `json:"permissions,omitempty"`
}

RoleUpdateParams edits a custom role. Nil fields are left unchanged; edits propagate to every member holding the role.

type SMTPIMAPBulkParams added in v0.3.0

type SMTPIMAPBulkParams struct {
	Accounts []SMTPIMAPParams `json:"accounts"`
}

SMTPIMAPBulkParams connects up to MaxSMTPIMAPBulkRows SMTP/IMAP mailboxes in one call.

type SMTPIMAPCredentialsParams added in v0.3.0

type SMTPIMAPCredentialsParams struct {
	SMTP *MailboxCredentials `json:"smtp"`
	IMAP *MailboxCredentials `json:"imap"`
}

SMTPIMAPCredentialsParams carries replacement credentials for an existing SMTP/IMAP mailbox. The address and display name never change on a reconnect.

type SMTPIMAPParams

type SMTPIMAPParams struct {
	Email string `json:"email"`
	Name  string `json:"name,omitempty"`
	// SMTP is used for sending and IMAP for reading. Both are required.
	SMTP *MailboxCredentials `json:"smtp"`
	IMAP *MailboxCredentials `json:"imap"`
}

SMTPIMAPParams connects a mailbox by its own SMTP and IMAP credentials, for providers with no OAuth integration. The passwords are sealed server-side on receipt and never returned.

type SSORedirect added in v0.3.0

type SSORedirect struct {
	// URL is the provider's authorization URL. Navigate the browser there.
	URL string `json:"url"`
	// Binding is the secret this client must keep (never in a URL) and hand
	// to [AuthService.ExchangeSSO]. It ties the handoff to the browser that
	// started the sign-in, so a forwarded handoff link cannot sign anyone in.
	Binding string `json:"binding"`
}

SSORedirect is where to send the browser to start a provider sign-in, plus the binding secret that must come back at the exchange.

type ScheduleDeletionParams

type ScheduleDeletionParams struct {
	Confirmation string `json:"confirmation"`
	Reason       string `json:"reason,omitempty"`
}

ScheduleDeletionParams schedules a delayed hard delete. Confirmation must match the DangerZoneStatus.ConfirmationHint exactly.

type ScheduleWindows

type ScheduleWindows [7][]TimeInterval

ScheduleWindows is a campaign's per-day sending schedule, indexed by weekday with Sunday at 0. An empty day means no sending that day. When any day is populated this supersedes the legacy Days/StartTime/EndTime fields.

func (ScheduleWindows) IsEmpty

func (w ScheduleWindows) IsEmpty() bool

IsEmpty reports whether no day carries an interval, in which case the legacy day/time fields apply.

type ScheduledDeletion

type ScheduledDeletion struct {
	ID             string  `json:"id"`
	ResourceType   string  `json:"resource_type"`
	ResourceID     string  `json:"resource_id"`
	OrganizationID *string `json:"organization_id,omitempty"`

	RequestedByUserID string  `json:"requested_by_user_id"`
	Reason            *string `json:"reason,omitempty"`

	ScheduledAt  time.Time `json:"scheduled_at"`
	ExecuteAfter time.Time `json:"execute_after"`
	GraceDays    int       `json:"grace_days"`
	// Status is the deletion's lifecycle state: one of the DeletionStatus*
	// constants.
	Status string `json:"status"`

	CancelledAt       *time.Time `json:"cancelled_at,omitempty"`         //nolint:misspell // wire value: the API sends "cancelled" here
	CancelledByUserID *string    `json:"cancelled_by_user_id,omitempty"` //nolint:misspell // wire value: the API sends "cancelled" here
	CancelledReason   *string    `json:"cancelled_reason,omitempty"`     //nolint:misspell // wire value: the API sends "cancelled" here

	ExecutedAt     *time.Time `json:"executed_at,omitempty"`
	ExecutionError *string    `json:"execution_error,omitempty"`
	LastReminderAt *time.Time `json:"last_reminder_at,omitempty"`
}

ScheduledDeletion is a pending hard delete, cancelable until ExecuteAfter.

type ScheduledSend

type ScheduledSend struct {
	TaskID       string    `json:"task_id"`
	ScheduledAt  time.Time `json:"scheduled_at"`
	CreatedAt    time.Time `json:"created_at,omitempty"`
	AccountID    string    `json:"account_id"`
	AccountEmail string    `json:"account_email,omitempty"`
	AccountName  string    `json:"account_name,omitempty"`
	To           []string  `json:"to,omitempty"`
	CC           []string  `json:"cc,omitempty"`
	BCC          []string  `json:"bcc,omitempty"`
	Subject      string    `json:"subject"`
	Snippet      string    `json:"snippet,omitempty"`
	// ThreadID is the conversation the message will land in, when it was
	// queued as a reply.
	ThreadID string `json:"thread_id,omitempty"`
}

ScheduledSend is a queued outbound message that has not left yet.

type Segment added in v0.3.0

type Segment struct {
	ID             string  `json:"id"`
	OrganizationID string  `json:"organization_id"`
	CreatedBy      *string `json:"created_by,omitempty"`
	Name           string  `json:"name"`
	Description    string  `json:"description"`
	// Color is a #rrggbb value, lower-cased by the server.
	Color      string             `json:"color"`
	Match      SegmentMatch       `json:"match"`
	Conditions []SegmentCondition `json:"conditions"`

	// ContactCount is the live membership size (conditions plus overrides).
	ContactCount int `json:"contact_count"`
	// IncludedCount is how many contacts are pinned in by hand.
	IncludedCount int `json:"included_count"`
	// ExcludedCount is how many contacts are pinned out by hand.
	ExcludedCount int `json:"excluded_count"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Segment is a saved contact audience.

type SegmentAddToCampaignResult added in v0.3.0

type SegmentAddToCampaignResult struct {
	CampaignID string `json:"campaign_id"`
	// Added is how many leads were new to the campaign.
	Added int `json:"added"`
	// Members is the segment's membership size at enroll time; Members minus
	// Added were already in the campaign.
	Members int `json:"members"`
}

SegmentAddToCampaignResult reports a snapshot enrollment.

type SegmentCondition added in v0.3.0

type SegmentCondition struct {
	Field    string          `json:"field"`
	Operator SegmentOperator `json:"operator"`
	// Value is the scalar operand, always sent as a string: "30" for a day
	// count, "3" for a counter, "2026-01-15" for a date.
	Value string `json:"value,omitempty"`
	// Values is the list operand for in/not_in: enum options, or category,
	// campaign or segment IDs.
	Values []string `json:"values,omitempty"`
}

SegmentCondition is one predicate over a contact. Field names a filterable field (see SegmentService.Fields) or a custom field as SegmentCustomFieldPrefix plus its key; Operator picks the comparison. Scalar operators read Value, list operators read Values, and the valueless operators read neither. The server normalizes what it stores: dates become RFC 3339, numbers are canonicalized, and whichever of Value/Values the operator does not use is dropped.

type SegmentCreateParams added in v0.3.0

type SegmentCreateParams struct {
	Name        string  `json:"name"`
	Description *string `json:"description,omitempty"`
	// Color is a #rrggbb value.
	Color      *string            `json:"color,omitempty"`
	Match      *SegmentMatch      `json:"match,omitempty"`
	Conditions []SegmentCondition `json:"conditions,omitempty"`
}

SegmentCreateParams creates a segment. Name is required; everything else has a server default (no description, color #0284c7, match all, no conditions). A name already used by another segment is rejected with a 409.

type SegmentFieldKind added in v0.3.0

type SegmentFieldKind string

SegmentFieldKind groups filterable fields by the operators they accept. The per-kind operator sets are listed on the SegmentOperator constants.

const (
	// SegmentFieldText fields (first_name, last_name, email, email_domain,
	// phone, company, custom.*) take the text operators; comparisons ignore
	// case.
	SegmentFieldText SegmentFieldKind = "text"
	// SegmentFieldEnum fields (source, verification_status, esp_provider)
	// take in/not_in with Values drawn from [SegmentFieldSpec.Options].
	SegmentFieldEnum SegmentFieldKind = "enum"
	// SegmentFieldBool fields (subscribed, suppressed, is_catch_all) take
	// is_true/is_false and no value.
	SegmentFieldBool SegmentFieldKind = "bool"
	// SegmentFieldDate fields (created_at, updated_at, last_sent_at,
	// last_opened_at, last_clicked_at, last_replied_at) take within_days,
	// not_within_days, before, after, is_empty and is_not_empty.
	SegmentFieldDate SegmentFieldKind = "date"
	// SegmentFieldNumber fields (campaign_count, emails_sent, emails_opened,
	// emails_clicked, emails_replied, emails_bounced) take the comparison
	// operators with a whole-number Value. Engagement counters add up every
	// campaign the contact has been in; opens count human opens only.
	SegmentFieldNumber SegmentFieldKind = "number"
	// SegmentFieldCategory is the "category" field: in/not_in over category
	// IDs, plus is_empty/is_not_empty.
	SegmentFieldCategory SegmentFieldKind = "category"
	// SegmentFieldCampaign is the "campaign" field: in/not_in over campaign
	// IDs, plus is_empty/is_not_empty.
	SegmentFieldCampaign SegmentFieldKind = "campaign"
	// SegmentFieldSegment is the "segment" field: in/not_in over other
	// segment IDs. References nest at most five levels deep and may not form
	// a loop or point at the segment itself.
	SegmentFieldSegment SegmentFieldKind = "segment"
)

type SegmentFieldSpec added in v0.3.0

type SegmentFieldSpec struct {
	// Field is the value to put in [SegmentCondition.Field].
	Field string `json:"field"`
	// Label is the human-readable name, for a condition builder.
	Label string `json:"label"`
	// Group is the display group: "Contact", "Company", "Campaign
	// activity", "Email engagement", "Segments" or "Custom field".
	Group string           `json:"group"`
	Kind  SegmentFieldKind `json:"kind"`
	// Options lists the accepted Values of an enum field; nil otherwise.
	Options []string `json:"options,omitempty"`
}

SegmentFieldSpec describes one field a condition may filter on. Kind determines the accepted operators and operand shape.

type SegmentMatch added in v0.3.0

type SegmentMatch string

SegmentMatch says how a segment combines its conditions.

const (
	// SegmentMatchAll requires every condition to hold (AND). The default.
	SegmentMatchAll SegmentMatch = "all"
	// SegmentMatchAny requires at least one condition to hold (OR).
	SegmentMatchAny SegmentMatch = "any"
)

type SegmentMemberMode added in v0.3.0

type SegmentMemberMode string

SegmentMemberMode is a manual override on one contact's membership. It takes precedence over the conditions: an included contact is a member whether or not it matches, an excluded one never is.

const (
	// SegmentMemberInclude pins the contact into the segment.
	SegmentMemberInclude SegmentMemberMode = "include"
	// SegmentMemberExclude pins the contact out of the segment.
	SegmentMemberExclude SegmentMemberMode = "exclude"
	// SegmentMemberAuto clears the override so the conditions decide again.
	// It is only ever sent; a lookup never reports it.
	SegmentMemberAuto SegmentMemberMode = "auto"
)

type SegmentOperator added in v0.3.0

type SegmentOperator string

SegmentOperator is the comparison a SegmentCondition applies. Which operators a field accepts depends on its SegmentFieldKind; the server rejects a mismatch with a 400 naming the offending condition.

const (
	// SegmentOpEquals: text and number fields.
	SegmentOpEquals SegmentOperator = "equals"
	// SegmentOpNotEquals: text and number fields.
	SegmentOpNotEquals SegmentOperator = "not_equals"
	// SegmentOpContains: text fields, case-insensitive substring.
	SegmentOpContains SegmentOperator = "contains"
	// SegmentOpNotContains: text fields.
	SegmentOpNotContains SegmentOperator = "not_contains"
	// SegmentOpStartsWith: text fields.
	SegmentOpStartsWith SegmentOperator = "starts_with"
	// SegmentOpEndsWith: text fields.
	SegmentOpEndsWith SegmentOperator = "ends_with"
	// SegmentOpBefore: date fields; Value is YYYY-MM-DD or RFC 3339.
	SegmentOpBefore SegmentOperator = "before"
	// SegmentOpAfter: date fields; Value is YYYY-MM-DD or RFC 3339.
	SegmentOpAfter SegmentOperator = "after"
	// SegmentOpWithinDays: date fields; Value is a day count from 1 to 3650.
	SegmentOpWithinDays SegmentOperator = "within_days"
	// SegmentOpNotWithinDays: date fields; Value is a day count from 1 to
	// 3650. A contact with no date at all also matches.
	SegmentOpNotWithinDays SegmentOperator = "not_within_days"
	// SegmentOpGT: number fields.
	SegmentOpGT SegmentOperator = "gt"
	// SegmentOpGTE: number fields.
	SegmentOpGTE SegmentOperator = "gte"
	// SegmentOpLT: number fields.
	SegmentOpLT SegmentOperator = "lt"
	// SegmentOpLTE: number fields.
	SegmentOpLTE SegmentOperator = "lte"
)

Scalar operators read SegmentCondition.Value.

const (
	// SegmentOpIn: enum, category, campaign and segment fields.
	SegmentOpIn SegmentOperator = "in"
	// SegmentOpNotIn: enum, category, campaign and segment fields.
	SegmentOpNotIn SegmentOperator = "not_in"
)

List operators read SegmentCondition.Values and need at least one entry.

const (
	// SegmentOpIsEmpty: text, date, category and campaign fields.
	SegmentOpIsEmpty SegmentOperator = "is_empty"
	// SegmentOpIsNotEmpty: text, date, category and campaign fields.
	SegmentOpIsNotEmpty SegmentOperator = "is_not_empty"
	// SegmentOpIsTrue: bool fields.
	SegmentOpIsTrue SegmentOperator = "is_true"
	// SegmentOpIsFalse: bool fields.
	SegmentOpIsFalse SegmentOperator = "is_false"
)

Valueless operators ignore both Value and Values.

type SegmentOverride added in v0.3.0

type SegmentOverride struct {
	ContactID string `json:"contact_id"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
	Email     string `json:"email"`
	Company   string `json:"company"`
	// Mode is [SegmentMemberInclude] or [SegmentMemberExclude].
	Mode SegmentMemberMode `json:"mode"`
	// CreatedAt is when the override was set.
	CreatedAt time.Time `json:"created_at"`
}

SegmentOverride is one contact pinned into or out of a segment by hand.

type SegmentPreviewParams added in v0.3.0

type SegmentPreviewParams struct {
	// ID, when set, keeps that segment's manual overrides in the count, so
	// editing an existing segment previews the number its members will see.
	// It also lets the definition be checked for self-reference.
	ID *string `json:"id,omitempty"`
	// Match defaults to [SegmentMatchAll] when empty.
	Match      SegmentMatch       `json:"match,omitempty"`
	Conditions []SegmentCondition `json:"conditions"`
}

SegmentPreviewParams is an unsaved definition to count.

type SegmentPreviewResult added in v0.3.0

type SegmentPreviewResult struct {
	// ContactCount is how many contacts the definition matches right now.
	ContactCount int `json:"contact_count"`
}

SegmentPreviewResult is the answer to a preview.

type SegmentService added in v0.3.0

type SegmentService service

SegmentService manages segments: saved, reusable contact audiences. A segment is a list of conditions over contacts plus per-contact manual overrides. Membership is evaluated live on every read, so a segment never needs rebuilding and its counts are always current.

A contact is a member when it matches the conditions (per Segment.Match) or is manually included, and is not manually excluded. A segment with no conditions holds only its manual includes.

Segments read and write under the contact scopes, since a segment is a view over contacts. The one exception is SegmentService.AddToCampaign, which writes leads and therefore takes the campaign write scope.

Two ways to feed a campaign from a segment:

  • SegmentService.AddToCampaign is a snapshot: it enrolls the members at the moment of the call and nothing more.
  • Linking the segment to the campaign (PUT /campaigns/:id/segments, see CampaignService) is a live audience: members are enrolled immediately and contacts that later join the segment are enrolled as they appear, keeping a continuous campaign fed without further calls.

Contact search and export accept segment IDs to scope any contact query to a segment, and GET /contacts/:id/segments gives the contact-side view.

func (*SegmentService) AddToCampaign added in v0.3.0

func (s *SegmentService) AddToCampaign(ctx context.Context, id, campaignID string, opts ...RequestOption) (*SegmentAddToCampaignResult, *Response, error)

AddToCampaign enrolls every current member of the segment as a lead in the campaign. Contacts already in the campaign are skipped, each new lead gets a campaign_added activity, and a running campaign is woken so the leads are scheduled. Safe to retry.

This is a snapshot: contacts that join the segment later are not added until the call is repeated. For a live link that keeps enrolling members as they appear, link the segment to the campaign instead (PUT /campaigns/:id/segments via CampaignService).

Unlike the rest of the service this call takes the campaign write scope.

func (*SegmentService) Create added in v0.3.0

func (s *SegmentService) Create(ctx context.Context, params *SegmentCreateParams, opts ...RequestOption) (*Segment, *Response, error)

Create saves a segment and returns it with its initial live counts.

func (*SegmentService) Delete added in v0.3.0

func (s *SegmentService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes a segment and its overrides. Contacts are untouched. It fails with a 409 while another segment's conditions reference this one or a campaign has it linked as a live audience; detach it first.

func (*SegmentService) Fields added in v0.3.0

Fields returns every field a condition may filter on: the built-in catalog followed by the workspace's custom fields, each as "custom.<key>" with kind text. Use it to validate conditions client-side or to drive a condition builder.

func (*SegmentService) Get added in v0.3.0

func (s *SegmentService) Get(ctx context.Context, id string, opts ...RequestOption) (*Segment, *Response, error)

Get returns one segment with live counts.

func (*SegmentService) List added in v0.3.0

func (s *SegmentService) List(ctx context.Context, opts ...RequestOption) ([]Segment, *Response, error)

List returns every segment in the organization with live counts. The endpoint is not paginated: a workspace holds at most 200 segments.

func (*SegmentService) MemberModes added in v0.3.0

func (s *SegmentService) MemberModes(ctx context.Context, id string, contactIDs []string, opts ...RequestOption) (map[string]SegmentMemberMode, *Response, error)

MemberModes reports the manual override on each of the given contacts, keyed by contact ID. Contacts with no override are absent from the map, so a missing key means the conditions alone decide. Up to SegmentMaxMemberBatch IDs per call.

func (*SegmentService) Overrides added in v0.3.0

func (s *SegmentService) Overrides(ctx context.Context, id string, opts ...RequestOption) ([]SegmentOverride, *Response, error)

Overrides lists every contact pinned into or out of the segment, includes first, newest first, capped at SegmentMaxOverrides.

func (*SegmentService) Preview added in v0.3.0

Preview counts the contacts an unsaved definition matches, without creating anything. Invalid conditions come back as a 400 naming the condition, so it doubles as a validator.

func (*SegmentService) SetMembers added in v0.3.0

func (s *SegmentService) SetMembers(ctx context.Context, id string, contactIDs []string, mode SegmentMemberMode, opts ...RequestOption) (int, *Response, error)

SetMembers writes one manual override on a batch of contacts: SegmentMemberInclude pins them in, SegmentMemberExclude pins them out, SegmentMemberAuto clears any override so the conditions decide again. Up to SegmentMaxMemberBatch IDs per call; IDs outside the organization are ignored. It returns how many rows changed. Pinning in (or clearing an exclude) can admit new members, so linked campaigns are synced afterwards.

func (*SegmentService) Update added in v0.3.0

func (s *SegmentService) Update(ctx context.Context, id string, params *SegmentUpdateParams, opts ...RequestOption) (*Segment, *Response, error)

Update changes a segment's definition. Changing the conditions changes membership immediately, and any campaign the segment is linked to enrolls the new members on its next sync.

type SegmentUpdateParams added in v0.3.0

type SegmentUpdateParams struct {
	Name        *string       `json:"name,omitempty"`
	Description *string       `json:"description,omitempty"`
	Color       *string       `json:"color,omitempty"`
	Match       *SegmentMatch `json:"match,omitempty"`
	// Conditions replaces the whole condition list. A nil slice leaves the
	// conditions unchanged; an empty, non-nil slice ([]SegmentCondition{})
	// removes them all, leaving a segment of manual includes only.
	Conditions []SegmentCondition `json:"conditions"`
}

SegmentUpdateParams changes a segment. Nil fields are unchanged.

type SendEmailParams

type SendEmailParams struct {
	To        []string `json:"to"`
	CC        []string `json:"cc,omitempty"`
	BCC       []string `json:"bcc,omitempty"`
	Subject   string   `json:"subject"`
	BodyHTML  string   `json:"body_html,omitempty"`
	BodyPlain string   `json:"body_plain,omitempty"`
	// InReplyTo holds the RFC 5322 Message-IDs this message replies to.
	InReplyTo []string `json:"in_reply_to,omitempty"`
	// ThreadID continues an existing provider thread.
	ThreadID string `json:"thread_id,omitempty"`
	// SendMode is [SendModeInstant] (the default), [SendModeSmart] or
	// [SendModeScheduled].
	SendMode string `json:"send_mode,omitempty"`
	// ScheduledAt is the send time, required when SendMode is
	// [SendModeScheduled] and ignored otherwise. It must be in the future and
	// within 29 days.
	ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
}

SendEmailParams are the parameters for sending a one-off message from a mailbox.

type SendLifecycleState added in v0.3.0

type SendLifecycleState struct {
	// State is [SendLifecycleActive], [SendLifecycleResting] or
	// [SendLifecycleReserve].
	State string `json:"state"`
	// Since is when the mailbox entered State.
	Since *time.Time `json:"since,omitempty"`
	// Reason explains the transition, for example "held back by its owner".
	Reason string `json:"reason,omitempty"`
}

SendLifecycleState is whether a mailbox is offered to campaign sending, separate from its warmup health and from which worker hosts it.

func (*SendLifecycleState) SendsCold added in v0.3.0

func (s *SendLifecycleState) SendsCold() bool

SendsCold reports whether the mailbox is in campaign rotation.

type SendResult

type SendResult struct {
	// TaskID identifies the queued send.
	TaskID string `json:"task_id"`
	// ScheduledAt is when the message will actually go out: immediately for
	// [SendModeInstant], the next gap for [SendModeSmart], or the requested
	// time for [SendModeScheduled].
	ScheduledAt time.Time `json:"scheduled_at"`
	SendMode    string    `json:"send_mode"`
}

SendResult is returned when a message has been accepted for delivery.

type SendTimeOptimizationSettings

type SendTimeOptimizationSettings struct {
	Enabled                bool   `json:"enabled"`
	UseContactTimezone     bool   `json:"use_contact_timezone"`
	DefaultContactTimezone string `json:"default_contact_timezone"`
	// PreferredHours are local hours (0-23) to favor.
	PreferredHours          []int   `json:"preferred_hours"`
	WeekendWeightMultiplier float64 `json:"weekend_weight_multiplier"`
}

SendTimeOptimizationSettings holds each campaign email until the clock where the recipient reads mail reaches a preferred hour. It only ever delays a send, never brings one forward, and is off by default because turning it on changes when everything sends.

type SendingBehavior added in v0.3.0

type SendingBehavior struct {
	EmailAccountID string `json:"email_account_id"`
	// Enabled is off by default; a mailbox that has not opted in keeps its
	// fixed daily cap and minimum gap exactly as before.
	Enabled bool `json:"enabled"`

	// DailyLimitMin and DailyLimitMax bound the day's cold-send target
	// (defaults 30 to 45; 1 to 500).
	DailyLimitMin int `json:"daily_limit_min"`
	DailyLimitMax int `json:"daily_limit_max"`

	// HourlyLimitMin and HourlyLimitMax bound the hourly ceiling (defaults 5
	// to 9; 1 to 200), which stops the whole day landing in one burst.
	HourlyLimitMin int `json:"hourly_limit_min"`
	HourlyLimitMax int `json:"hourly_limit_max"`

	// GapMinSeconds and GapMaxSeconds bound the delay between sends, drawn
	// fresh for every send (defaults 90 to 420; 30 seconds to 24 hours).
	GapMinSeconds int `json:"gap_min_seconds"`
	GapMaxSeconds int `json:"gap_max_seconds"`

	// WorkStartMin and WorkStartMax bound when the mailbox opens (defaults
	// 09:03 to 09:27); WorkEndMin and WorkEndMax bound when it closes
	// (defaults 17:18 to 17:56). The latest start must precede the earliest
	// end.
	WorkStartMin int `json:"work_start_min"`
	WorkStartMax int `json:"work_start_max"`
	WorkEndMin   int `json:"work_end_min"`
	WorkEndMax   int `json:"work_end_max"`

	// LunchEnabled (on by default) carves a quiet gap out of the day that
	// starts between LunchEarliest and LunchLatest (defaults 12:00 to 13:30)
	// and lasts LunchMinMinutes to LunchMaxMinutes (defaults 30 to 60, at most
	// 240). The break must fit inside the shortest workday the ranges can
	// produce.
	LunchEnabled    bool `json:"lunch_enabled"`
	LunchEarliest   int  `json:"lunch_earliest"`
	LunchLatest     int  `json:"lunch_latest"`
	LunchMinMinutes int  `json:"lunch_min_minutes"`
	LunchMaxMinutes int  `json:"lunch_max_minutes"`

	// Weekdays is the Monday-indexed bitmask of sending days, cold and warmup
	// alike (default [BehaviorWeekdays]). An enabled profile needs at least
	// one day.
	Weekdays int `json:"weekdays"`

	// Timezone is a read-only echo of the mailbox's timezone.
	Timezone string `json:"timezone,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

SendingBehavior is a mailbox's humanlike sending profile: the ranges a workday is rolled from, not the rolled values (see DailyPlan). Each local day the mailbox rolls one start, finish, break, daily target and hourly ceiling from these ranges and keeps to them, so it behaves like one person having one workday rather than a process re-deciding its schedule.

Every minute-of-day value is minutes since local midnight in the mailbox's own timezone. Behavior can only ever lower volume or delay a send: the rolled daily target is applied as a minimum against the mailbox and campaign caps, and the gap range replaces the mailbox's fixed minimum wait.

Disabled profiles are still stored, so the ranges can be tuned before switching the profile on. A mailbox that has never been configured reads back the defaults.

func (*SendingBehavior) WorksOn added in v0.3.0

func (b *SendingBehavior) WorksOn(wd time.Weekday) bool

WorksOn reports whether the profile sends on the given weekday.

type SendingBehaviorUpdateParams added in v0.3.0

type SendingBehaviorUpdateParams struct {
	Enabled *bool `json:"enabled,omitempty"`

	DailyLimitMin *int `json:"daily_limit_min,omitempty"`
	DailyLimitMax *int `json:"daily_limit_max,omitempty"`

	HourlyLimitMin *int `json:"hourly_limit_min,omitempty"`
	HourlyLimitMax *int `json:"hourly_limit_max,omitempty"`

	GapMinSeconds *int `json:"gap_min_seconds,omitempty"`
	GapMaxSeconds *int `json:"gap_max_seconds,omitempty"`

	WorkStartMin *int `json:"work_start_min,omitempty"`
	WorkStartMax *int `json:"work_start_max,omitempty"`
	WorkEndMin   *int `json:"work_end_min,omitempty"`
	WorkEndMax   *int `json:"work_end_max,omitempty"`

	LunchEnabled    *bool `json:"lunch_enabled,omitempty"`
	LunchEarliest   *int  `json:"lunch_earliest,omitempty"`
	LunchLatest     *int  `json:"lunch_latest,omitempty"`
	LunchMinMinutes *int  `json:"lunch_min_minutes,omitempty"`
	LunchMaxMinutes *int  `json:"lunch_max_minutes,omitempty"`

	Weekdays *int `json:"weekdays,omitempty"`
}

SendingBehaviorUpdateParams is a partial update to a SendingBehavior. Nil fields keep their stored value, so Enabled can be toggled without resending every range. A range that breaks a cross-field rule is rejected with a 400 naming the field. Editing the ranges does not change today's rolled plan; the new ranges take effect on the next roll.

type Session

type Session struct {
	AccessToken           string    `json:"access_token"`
	AccessTokenExpiresAt  time.Time `json:"access_token_expires_at"`
	RefreshToken          string    `json:"refresh_token"`
	RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`

	// TwoFARequired is true when the account has two-factor enabled. The token
	// fields are then empty; finish with [AuthService.VerifyTwoFA] using
	// PendingToken.
	TwoFARequired bool `json:"two_fa_required,omitempty"`
	// PendingToken is the single-use handle for the 2FA step.
	PendingToken string `json:"pending_token,omitempty"`
	// ExpiresIn is how long PendingToken remains valid, in seconds.
	ExpiresIn int `json:"expires_in,omitempty"`
}

Session is a signed-in session's token pair.

type SetupParams added in v0.3.0

type SetupParams struct {
	Token     string `json:"token"`
	Email     string `json:"email"`
	Password  string `json:"password"`
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
}

SetupParams claims a fresh self-hosted instance. Token is the one-time setup token printed at first boot (or by warmblyctl setup-link); the rest becomes the owner account.

type SkillCreateParams

type SkillCreateParams struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Content     string `json:"content,omitempty"`
	// Enabled defaults to true when nil.
	Enabled *bool `json:"enabled,omitempty"`
}

SkillCreateParams creates a skill. Name is required.

type SkillService

type SkillService service

SkillService manages AI skills: workspace playbooks folded into every AI writing surface, on top of the voice profile on Organization.

An enabled skill applies everywhere the assistant writes, so keep them short and behavioral — "never open with 'I hope this finds you well'" — rather than piling in per-campaign detail.

func (*SkillService) Create

func (s *SkillService) Create(ctx context.Context, params *SkillCreateParams, opts ...RequestOption) (*AISkill, *Response, error)

Create adds an AI skill.

func (*SkillService) Delete

func (s *SkillService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes an AI skill.

func (*SkillService) List

func (s *SkillService) List(ctx context.Context, opts ...RequestOption) ([]AISkill, *Response, error)

List returns the workspace's AI skills.

func (*SkillService) Update

func (s *SkillService) Update(ctx context.Context, id string, params *SkillUpdateParams, opts ...RequestOption) (*AISkill, *Response, error)

Update modifies an AI skill.

type SkillUpdateParams

type SkillUpdateParams struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	Content     *string `json:"content,omitempty"`
	Enabled     *bool   `json:"enabled,omitempty"`
}

SkillUpdateParams updates a skill. Nil fields are left unchanged.

type Spreadsheet

type Spreadsheet struct {
	SheetID string           `json:"sheet_id"`
	Title   string           `json:"title"`
	Tabs    []SpreadsheetTab `json:"tabs"`
}

Spreadsheet is a workbook's title and its tabs.

type SpreadsheetTab

type SpreadsheetTab struct {
	Title string `json:"title"`
	Index int    `json:"index"`
}

SpreadsheetTab is one tab in a workbook.

type StageCreateParams

type StageCreateParams struct {
	Name  string `json:"name"`
	Color string `json:"color"`
}

StageCreateParams creates one pipeline stage.

type StageUpdateParams

type StageUpdateParams struct {
	Name  *string `json:"name,omitempty"`
	Color *string `json:"color,omitempty"`
}

StageUpdateParams updates a stage. Nil fields are left unchanged.

type Step

type Step struct {
	ID   string `json:"id"`
	Name string `json:"name"`

	Subject   string `json:"subject"`
	BodyPlain string `json:"body_plain"`
	BodyHTML  string `json:"body_html"`
	// BodySync keeps the plain-text body derived from the HTML body.
	BodySync bool `json:"body_sync"`
	// BodyCode reports whether the HTML body is edited as raw markup.
	BodyCode bool `json:"body_code"`

	// WaitAfter is the delay in minutes before the next step runs.
	WaitAfter int `json:"wait_after"`
	// Position is the step's zero-based index in the sequence. It orders the
	// canvas and picks the entry step; it never advances a contact by itself.
	Position int `json:"position"`

	// X and Y are the step's coordinates on the sequence canvas. They are
	// written only through [CampaignService.UpdateStepLayout].
	X float64 `json:"x"`
	Y float64 `json:"y"`

	// Conditions is the step's routing: the connections out of it, evaluated
	// against the contact's engagement to pick the next step. Routing follows
	// connections only, so a step with an empty tree (no branches) has no
	// outgoing path and the contact's flow ends there; a plain "go there next"
	// link is a branch with no conditions. It is left as raw JSON because the
	// branch grammar evolves independently of this SDK.
	Conditions json.RawMessage `json:"conditions,omitempty"`

	// Kind is [StepKindEmail], [StepKindAction] or [StepKindWait].
	Kind string `json:"kind"`
	// Action is the typed configuration for a non-email node, as raw JSON. It
	// is an empty object for email steps.
	Action json.RawMessage `json:"action,omitempty"`

	UpdatedAt time.Time `json:"updated_at"`
	CreatedAt time.Time `json:"created_at"`
}

Step is a single step in a campaign's sequence.

type StepAnalytics

type StepAnalytics struct {
	StepID     string `json:"step_id"`
	Name       string `json:"name"`
	Position   int    `json:"position"`
	EmailsSent int64  `json:"emails_sent"`
	Opens      int64  `json:"opens"`
	Clicks     int64  `json:"clicks"`
	Replies    int64  `json:"replies"`
	Bounces    int64  `json:"bounces"`
}

StepAnalytics is one sequence step's engagement.

type StepInput

type StepInput struct {
	Name      string `json:"name,omitempty"`
	Subject   string `json:"subject,omitempty"`
	BodyPlain string `json:"body_plain,omitempty"`
	BodyHTML  string `json:"body_html,omitempty"`
	BodySync  *bool  `json:"body_sync,omitempty"`
	BodyCode  *bool  `json:"body_code,omitempty"`
	WaitAfter *int   `json:"wait_after,omitempty"`
}

StepInput is one step seeded during campaign creation.

type StepPosition

type StepPosition struct {
	ID string  `json:"id"`
	X  float64 `json:"x"`
	Y  float64 `json:"y"`
}

StepPosition is one step's coordinates on the sequence canvas.

type StepUpdateParams

type StepUpdateParams struct {
	Name      *string `json:"name,omitempty"`
	Subject   *string `json:"subject,omitempty"`
	BodyPlain *string `json:"body_plain,omitempty"`
	BodyHTML  *string `json:"body_html,omitempty"`
	BodySync  *bool   `json:"body_sync,omitempty"`
	BodyCode  *bool   `json:"body_code,omitempty"`
	WaitAfter *int    `json:"wait_after,omitempty"`

	// Conditions replaces the step's outgoing connections when non-nil. Send
	// an empty object to remove them all, after which the contact's flow ends
	// at this step.
	Conditions json.RawMessage `json:"conditions,omitempty"`

	// Kind and Action switch the node between an email and an action or wait.
	// A one-time campaign refuses a second email step.
	Kind   *string         `json:"kind,omitempty"`
	Action json.RawMessage `json:"action,omitempty"`
}

StepUpdateParams updates a campaign step. Nil fields are left unchanged.

type StorageUsage added in v0.3.0

type StorageUsage struct {
	UsedBytes  int64 `json:"used_bytes"`
	LimitBytes int64 `json:"limit_bytes"`
	OverQuota  bool  `json:"over_quota"`
}

StorageUsage is attachment storage used against the workspace's quota. A workspace that dropped to a smaller plan can be over quota with no upload refused yet, which is what OverQuota reports.

type Subscription

type Subscription struct {
	ID             string `json:"id"`
	UserID         string `json:"user_id"`
	OrganizationID string `json:"organization_id"`
	PlanID         string `json:"plan_id"`

	StripeCustomerID     string  `json:"stripe_customer_id,omitempty"`
	StripeSubscriptionID *string `json:"stripe_subscription_id,omitempty"`
	StripePriceID        *string `json:"stripe_price_id,omitempty"`

	// Status is the lifecycle state, for example "active", "trialing" or
	// "canceled".
	Status string `json:"status"`

	CurrentPeriodStart *time.Time `json:"current_period_start,omitempty"`
	CurrentPeriodEnd   *time.Time `json:"current_period_end,omitempty"`
	// CancelAtPeriodEnd is true when the subscription is set to lapse rather
	// than renew.
	CancelAtPeriodEnd bool       `json:"cancel_at_period_end"`
	CanceledAt        *time.Time `json:"canceled_at,omitempty"`

	TrialStart *time.Time `json:"trial_start,omitempty"`
	TrialEnd   *time.Time `json:"trial_end,omitempty"`

	// FreeTrialStartedAt and FreeTrialEndsAt are Warmbly's own free trial,
	// distinct from a provider-side trial.
	FreeTrialStartedAt *time.Time `json:"free_trial_started_at,omitempty"`
	FreeTrialEndsAt    *time.Time `json:"free_trial_ends_at,omitempty"`

	// IsEnterprise marks a subscription negotiated outside the plan catalog.
	IsEnterprise bool `json:"is_enterprise"`
	// Plan is the plan this subscription is on, joined in by the API so a
	// caller does not have to look it up in [MetaService.Plans].
	Plan *Plan `json:"plan,omitempty"`

	CreatedAt time.Time `json:"created_at,omitempty"`
	UpdatedAt time.Time `json:"updated_at,omitempty"`
}

Subscription is the workspace's current plan subscription.

type SubscriptionStatus

type SubscriptionStatus struct {
	HasSubscription    bool `json:"has_subscription"`
	IsInFreeTrial      bool `json:"is_in_free_trial"`
	IsFreeTrialExpired bool `json:"is_free_trial_expired"`
	IsPaidSubscriber   bool `json:"is_paid_subscriber"`
	// DailyEmailLimit is the send cap the server enforces: an approved limit
	// increase, else the plan's, else the trial's.
	DailyEmailLimit int   `json:"daily_email_limit"`
	Plan            *Plan `json:"plan,omitempty"`
}

SubscriptionStatus is the plan-gate view: what the workspace is entitled to.

type SubscriptionWithLimits added in v0.3.0

type SubscriptionWithLimits struct {
	Subscription
	RateLimits *RealtimeRateLimits `json:"rate_limits,omitempty"`
}

SubscriptionWithLimits is the subscription together with the realtime gateway ceilings its plan carries.

type SuppressedRecipient added in v0.3.0

type SuppressedRecipient struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	// Email is the suppressed address, or the bare domain when Kind is
	// [SuppressionKindDomain]. Always lower-case.
	Email  string            `json:"email"`
	Kind   SuppressionKind   `json:"kind"`
	Reason string            `json:"reason"`
	Source SuppressionSource `json:"source"`
	// CampaignID is the campaign whose send produced an automatic entry;
	// nil for manual and import entries.
	CampaignID *string `json:"campaign_id,omitempty"`
	// ExpiresAt, when set, is when the entry stops applying. Expired entries
	// are never listed. Entries added through the API do not expire.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	// Metadata carries provenance, such as "added_by" (the user who added a
	// manual entry) or "via" for an unsubscribe.
	Metadata  map[string]any `json:"metadata,omitempty"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
}

SuppressedRecipient is one entry on the suppression list.

type SuppressionAddParams added in v0.3.0

type SuppressionAddParams struct {
	// Entries holds up to [SuppressionMaxEntries] values. Duplicates within
	// the batch are collapsed.
	Entries []SuppressionEntry `json:"entries"`
	// Reason applies to every entry that does not carry its own. When both
	// are empty the server records a generic one.
	Reason string `json:"reason,omitempty"`
}

SuppressionAddParams adds addresses and domains to the list.

type SuppressionAddResult added in v0.3.0

type SuppressionAddResult struct {
	// Added is how many entries were written, counting values already on
	// the list that were updated in place.
	Added int `json:"added"`
	// Skipped lists the values, as sent, that were neither a valid address
	// nor a valid domain. They do not fail the request.
	Skipped []string `json:"skipped"`
}

SuppressionAddResult reports what an Add did.

type SuppressionEntry added in v0.3.0

type SuppressionEntry struct {
	// Value is an address ("dana@acme.com") or a domain ("acme.com" or
	// "@acme.com"). Anything with an "@" after a leading one is stripped is
	// treated as an address; a bare host is a domain. The server decides
	// the kind: there is no way to force a value to one or the other.
	Value string `json:"value"`
	// Reason overrides [SuppressionAddParams.Reason] for this entry.
	Reason string `json:"reason,omitempty"`
}

SuppressionEntry is one value to suppress.

type SuppressionKind added in v0.3.0

type SuppressionKind string

SuppressionKind says what a suppression entry matches.

const (
	// SuppressionKindEmail matches one address.
	SuppressionKindEmail SuppressionKind = "email"
	// SuppressionKindDomain matches every address at a domain; the entry's
	// Email holds the bare host.
	SuppressionKindDomain SuppressionKind = "domain"
)

type SuppressionListParams added in v0.3.0

type SuppressionListParams struct {
	ListOptions
	// Query is a case-insensitive substring filter on the address or
	// domain.
	Query string `json:"-"`
}

SuppressionListParams filters and pages the suppression list. Pages are newest first; Limit is capped at 200 and defaults to 50.

type SuppressionService added in v0.3.0

type SuppressionService service

SuppressionService manages the workspace suppression list: every address and domain that no campaign will email, whatever put it there. The list is checked before every campaign send.

Entries arrive two ways. Warmbly adds them automatically on a hard bounce, a spam complaint or an unsubscribe (sources bounce, complaint, unsubscribe); you add them by hand with SuppressionService.Add (sources manual and import). Lifting an entry with SuppressionService.Delete lets campaigns email the address, or every address at the domain, again.

Adding an address also switches off the matching contact's subscribed flag, and removing it switches the flag back on, so the list and the contact never disagree. Reads take the contact read scope, writes the contact write scope.

func (*SuppressionService) Add added in v0.3.0

Add puts addresses and domains on the list. A single entry is recorded with source manual, two or more with source import. Values already on the list are updated in place rather than rejected, so the call is safe to repeat; values that are neither an address nor a domain come back in SuppressionAddResult.Skipped. The batch is written in one transaction. Adding an address also unsubscribes the matching contact.

func (*SuppressionService) Delete added in v0.3.0

func (s *SuppressionService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete lifts one entry so campaigns can email the address (or every address at the domain) again. Removing an address entry also restores the matching contact's subscribed flag. The removal is audited with the entry's value, kind and source, since lifting an opt-out the recipient made themselves is what a compliance review looks for. Returns a 404 when the entry is not in this organization.

func (*SuppressionService) List added in v0.3.0

List pages the suppression list, newest first. Expired entries are omitted. Pass nil for the first page with defaults.

type SuppressionSource added in v0.3.0

type SuppressionSource string

SuppressionSource records what put an entry on the list.

const (
	// SuppressionSourceBounce: a permanent (5.x.x) delivery failure.
	SuppressionSourceBounce SuppressionSource = "bounce"
	// SuppressionSourceComplaint: the recipient marked a send as spam.
	SuppressionSourceComplaint SuppressionSource = "complaint"
	// SuppressionSourceUnsubscribe: the recipient opted out, by link, reply
	// keyword or List-Unsubscribe header.
	SuppressionSourceUnsubscribe SuppressionSource = "unsubscribe"
	// SuppressionSourceManual: added by hand, one entry at a time.
	SuppressionSourceManual SuppressionSource = "manual"
	// SuppressionSourceImport: added by hand as a batch of two or more.
	SuppressionSourceImport SuppressionSource = "import"
)

type SyncRun

type SyncRun struct {
	ID             string `json:"id"`
	ConnectionID   string `json:"connection_id"`
	OrganizationID string `json:"organization_id,omitempty"`
	// Kind is what ran, for example "push" or "pull".
	Kind string `json:"kind"`
	// Status is the outcome, for example "success" or "error".
	Status           string     `json:"status"`
	Detail           string     `json:"detail,omitempty"`
	RecordsProcessed int        `json:"records_processed"`
	StartedAt        time.Time  `json:"started_at"`
	FinishedAt       *time.Time `json:"finished_at,omitempty"`
}

SyncRun is one execution of a connection's sync.

type TaskReliabilitySettings

type TaskReliabilitySettings struct {
	Enabled                bool `json:"enabled"`
	DLQEnabled             bool `json:"dlq_enabled"`
	MaxAttempts            int  `json:"max_attempts"`
	ExecutionWindowSeconds int  `json:"execution_window_seconds"`
}

TaskReliabilitySettings tunes send-task retries and the dead-letter queue.

type TaskSearchParams

type TaskSearchParams struct {
	ListOptions

	// Query matches the task title.
	Query string `json:"query,omitempty"`
	// Statuses matches any of the TaskStatus* constants, Priorities any of the
	// TaskPriority* constants, and Types any [CRMTaskType] name.
	Statuses   []string `json:"statuses,omitempty"`
	Priorities []string `json:"priorities,omitempty"`
	Types      []string `json:"types,omitempty"`
	// AssignedTo matches any of the given assignee user ids. TeamIDs matches
	// tasks assigned to any of those teams, or whose assignee belongs to one.
	AssignedTo []string `json:"assigned_to,omitempty"`
	TeamIDs    []string `json:"team_ids,omitempty"`

	ContactID *string    `json:"contact_id,omitempty"`
	DealID    *string    `json:"deal_id,omitempty"`
	DueAfter  *time.Time `json:"due_after,omitempty"`
	DueBefore *time.Time `json:"due_before,omitempty"`
	// Overdue matches tasks past their due date that are neither completed nor
	// canceled.
	Overdue bool `json:"overdue,omitempty"`

	// SortBy is "created_at", "due_date", "priority", "title" or "updated_at".
	// Reverse switches to ascending; the default is descending.
	SortBy  string `json:"sort_by,omitempty"`
	Reverse bool   `json:"reverse,omitempty"`
}

TaskSearchParams is the faceted filter shared by CRMService.SearchTasks and CRMService.TasksSummary. Every facet is optional; slice facets match any of their values.

type TaskService

type TaskService service

TaskService inspects and replays the send-task dead-letter queue: work that exhausted its retries and parked instead of being dropped.

A replay re-dispatches real mail, so it needs the send permission.

func (*TaskService) DeadLetters

func (s *TaskService) DeadLetters(ctx context.Context, params *DeadLetterListParams, opts ...RequestOption) ([]DeadLetter, *Response, error)

DeadLetters returns parked tasks.

func (*TaskService) Replay

func (s *TaskService) Replay(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Replay re-dispatches a parked task. This sends real mail.

type TaskTypeCreateParams

type TaskTypeCreateParams struct {
	Name  string `json:"name"`
	Color string `json:"color,omitempty"`
}

TaskTypeCreateParams creates a task type.

type TaskTypeUpdateParams

type TaskTypeUpdateParams struct {
	Name     *string `json:"name,omitempty"`
	Color    *string `json:"color,omitempty"`
	Position *int    `json:"position,omitempty"`
}

TaskTypeUpdateParams updates a task type. Nil fields are left unchanged.

type TasksSummary

type TasksSummary struct {
	Total          int64 `json:"total"`
	PendingCount   int64 `json:"pending_count"`
	InProgress     int64 `json:"in_progress_count"`
	CompletedCount int64 `json:"completed_count"`
	CancelledCount int64 `json:"cancelled_count"` //nolint:misspell // wire value: the API sends "cancelled" here
	OverdueCount   int64 `json:"overdue_count"`
	HighPriority   int64 `json:"high_priority_count"`
}

TasksSummary aggregates every task matching a TaskSearchParams.

type Team

type Team struct {
	ID             string       `json:"id"`
	OrganizationID string       `json:"organization_id"`
	Name           string       `json:"name"`
	Color          string       `json:"color"`
	Members        []TeamMember `json:"members"`
	CreatedAt      time.Time    `json:"created_at"`
	UpdatedAt      time.Time    `json:"updated_at"`
}

Team is a named group of workspace members.

type TeamCreateParams

type TeamCreateParams struct {
	Name  string `json:"name"`
	Color string `json:"color,omitempty"`
}

TeamCreateParams creates a team.

type TeamMember

type TeamMember struct {
	UserID  string    `json:"user_id"`
	Email   string    `json:"email"`
	Name    string    `json:"name"`
	AddedAt time.Time `json:"added_at"`
}

TeamMember is one member of a team.

type TeamService

type TeamService service

TeamService groups existing workspace members into named teams, which CRM tasks and deals can be assigned to instead of a single person.

func (*TeamService) AddMember

func (s *TeamService) AddMember(ctx context.Context, id, userID string, opts ...RequestOption) (*Team, *Response, error)

AddMember adds an existing workspace member to a team.

func (*TeamService) Create

func (s *TeamService) Create(ctx context.Context, params *TeamCreateParams, opts ...RequestOption) (*Team, *Response, error)

Create creates a team.

func (*TeamService) Delete

func (s *TeamService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes a team. Its members keep their workspace membership.

func (*TeamService) Get

func (s *TeamService) Get(ctx context.Context, id string, opts ...RequestOption) (*Team, *Response, error)

Get retrieves a single team.

func (*TeamService) List

func (s *TeamService) List(ctx context.Context, opts ...RequestOption) ([]Team, *Response, error)

List returns the workspace's teams with their members.

func (*TeamService) RemoveMember

func (s *TeamService) RemoveMember(ctx context.Context, id, userID string, opts ...RequestOption) (*Response, error)

RemoveMember removes someone from a team.

func (*TeamService) Update

func (s *TeamService) Update(ctx context.Context, id string, params *TeamUpdateParams, opts ...RequestOption) (*Team, *Response, error)

Update renames or recolors a team.

type TeamUpdateParams

type TeamUpdateParams struct {
	Name  *string `json:"name,omitempty"`
	Color *string `json:"color,omitempty"`
}

TeamUpdateParams renames or recolors a team.

type Template

type Template struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	UserID         string `json:"user_id"`
	Name           string `json:"name"`
	Subject        string `json:"subject"`
	BodyHTML       string `json:"body_html"`
	BodyPlain      string `json:"body_plain"`
	// Position is the template's place in the organization's ordered list.
	Position  int       `json:"position"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Template is a reusable message template as returned by the API.

type TemplateCreateParams

type TemplateCreateParams struct {
	Name      string `json:"name"`
	Subject   string `json:"subject,omitempty"`
	BodyHTML  string `json:"body_html,omitempty"`
	BodyPlain string `json:"body_plain,omitempty"`
}

TemplateCreateParams creates a template. Only Name is required.

type TemplateField added in v0.3.0

type TemplateField struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

TemplateField is one templated key/value pair in an action config. Value is a Go template rendered against the event data, so "{{.data.company}}" reads a form answer and "{{.first_name}}" a contact field.

type TemplateIssue

type TemplateIssue struct {
	// Severity is [TemplateIssueWarn] or [TemplateIssueHigh].
	Severity string `json:"severity"`
	// Code is the stable identifier for the rule that fired.
	Code    string `json:"code"`
	Message string `json:"message"`
}

TemplateIssue is one problem found while scoring copy.

type TemplatePreviewAttachment added in v0.3.0

type TemplatePreviewAttachment struct {
	ID       string `json:"id"`
	Filename string `json:"filename"`
	Size     int64  `json:"size"`
	MimeType string `json:"mime_type"`
}

TemplatePreviewAttachment is one attachment a previewed send would carry.

type TemplatePreviewFrom added in v0.3.0

type TemplatePreviewFrom struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

TemplatePreviewFrom is the sender line of a rendered preview.

type TemplatePreviewInput

type TemplatePreviewInput struct {
	FirstName    string            `json:"first_name,omitempty"`
	LastName     string            `json:"last_name,omitempty"`
	Email        string            `json:"email,omitempty"`
	Company      string            `json:"company,omitempty"`
	Phone        string            `json:"phone,omitempty"`
	CustomFields map[string]string `json:"custom_fields,omitempty"`
}

TemplatePreviewInput is the sample contact merge tags are resolved against.

type TemplatePreviewParams

type TemplatePreviewParams struct {
	Subject   string `json:"subject,omitempty"`
	BodyHTML  string `json:"body_html,omitempty"`
	BodyPlain string `json:"body_plain,omitempty"`
	// Contact overrides individual fields of the contact rendered for (the
	// sample one, or the one named by ContactID).
	Contact *TemplatePreviewInput `json:"contact,omitempty"`
	// ContactID renders for a real contact of the organization. It requires
	// contact read access on top of the campaign permission.
	ContactID string `json:"contact_id,omitempty"`
	// CampaignID applies the campaign's opt-out footer and plain-text rule and
	// lists its campaign-wide attachments in the result.
	CampaignID string `json:"campaign_id,omitempty"`
	// AccountID applies that mailbox's signature and fills
	// [TemplatePreviewResult.From].
	AccountID string `json:"account_id,omitempty"`
	// StepID adds that step's own attachments to the campaign-wide ones. It
	// only means something with CampaignID.
	StepID string `json:"step_id,omitempty"`
}

TemplatePreviewParams renders campaign copy exactly as the send path would, so merge tags can be checked without sending anything. With no context it renders against a built-in sample contact; CampaignID, AccountID, ContactID and StepID add the pieces of a real send.

type TemplatePreviewResult

type TemplatePreviewResult struct {
	Subject   string `json:"subject"`
	BodyHTML  string `json:"body_html"`
	BodyPlain string `json:"body_plain"`
	// Errors are template syntax problems. They block sending.
	Errors []string `json:"errors,omitempty"`
	// Unresolved lists merge tags with no value on the contact.
	Unresolved []string `json:"unresolved,omitempty"`
	// From is the sender as the recipient will see it, when AccountID was
	// given.
	From *TemplatePreviewFrom `json:"from,omitempty"`
	// Attachments are the files the send would carry, metadata only.
	Attachments []TemplatePreviewAttachment `json:"attachments,omitempty"`
}

TemplatePreviewResult is the rendered copy plus anything that failed to resolve. Tracking rewrites are left out; everything else the send path adds (signature, opt-out footer, sender, attachments) is included when the request named the campaign and mailbox.

type TemplateRenderResult

type TemplateRenderResult struct {
	Subject   string `json:"subject"`
	BodyHTML  string `json:"body_html"`
	BodyPlain string `json:"body_plain"`
}

TemplateRenderResult is a template rendered against a set of variables.

type TemplateScore

type TemplateScore struct {
	Score  int             `json:"score"`
	Issues []TemplateIssue `json:"issues"`
}

TemplateScore rates copy for spam risk before it is ever sent. Score runs 0 to 100, higher being safer.

type TemplateScoreParams

type TemplateScoreParams struct {
	Subject   string `json:"subject,omitempty"`
	BodyHTML  string `json:"body_html,omitempty"`
	BodyPlain string `json:"body_plain,omitempty"`
}

TemplateScoreParams is the copy to score. Pass whichever parts exist.

type TemplateService

type TemplateService service

TemplateService manages the organization's reply templates: reusable subject and body snippets that can be rendered into a campaign step or a one-off reply. Templates are ordered, and the order is editable.

func (*TemplateService) Create

Create creates a new template, appended to the end of the list.

func (*TemplateService) Delete

func (s *TemplateService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete permanently deletes a template.

func (*TemplateService) Duplicate

func (s *TemplateService) Duplicate(ctx context.Context, id string, opts ...RequestOption) (*Template, *Response, error)

Duplicate copies a template, appending the copy to the end of the list.

func (*TemplateService) Get

func (s *TemplateService) Get(ctx context.Context, id string, opts ...RequestOption) (*Template, *Response, error)

Get retrieves a single template by ID.

func (*TemplateService) List

func (s *TemplateService) List(ctx context.Context, query string, opts ...RequestOption) ([]Template, *Response, error)

List returns the organization's templates in their configured order. Query is an optional case-insensitive search over name and subject.

func (*TemplateService) Render

func (s *TemplateService) Render(ctx context.Context, id string, variables map[string]string, opts ...RequestOption) (*TemplateRenderResult, *Response, error)

Render fills a template's merge tags from variables and returns the result. Nothing is sent.

func (*TemplateService) Reorder

func (s *TemplateService) Reorder(ctx context.Context, ids []string, opts ...RequestOption) ([]Template, *Response, error)

Reorder repositions the given templates in the listed order and returns the full reordered list. Templates not in ids keep their positions.

func (*TemplateService) Score

Score rates arbitrary copy for spam risk. It works on any subject and body, not just a stored template.

func (*TemplateService) Update

func (s *TemplateService) Update(ctx context.Context, id string, params *TemplateUpdateParams, opts ...RequestOption) (*Template, *Response, error)

Update modifies an existing template.

type TemplateUpdateParams

type TemplateUpdateParams struct {
	Name      *string `json:"name,omitempty"`
	Subject   *string `json:"subject,omitempty"`
	BodyHTML  *string `json:"body_html,omitempty"`
	BodyPlain *string `json:"body_plain,omitempty"`
}

TemplateUpdateParams updates a template. Nil fields are left unchanged.

type TestEmailParams

type TestEmailParams struct {
	// AccountID is the mailbox the test is sent from.
	AccountID string `json:"account_id"`
	Recipient string `json:"recipient"`
	// StepID selects which step to render; the first step is used when empty.
	StepID string `json:"step_id,omitempty"`
	// ContactID renders the copy for a real contact of the organization
	// instead of the built-in sample one. It requires contact read access on
	// top of the campaign permission.
	ContactID string `json:"contact_id,omitempty"`
}

TestEmailParams sends a rendered preview of a campaign step to one address. The test carries the step's attachments, the mailbox signature and the opt-out footer, rendered from what is saved (not unsaved edits). Opens and clicks on a test are not tracked and its opt-out link names nobody.

type TestEmailResult

type TestEmailResult struct {
	Message   string `json:"message"`
	Recipient string `json:"recipient"`
	Subject   string `json:"subject"`
	AccountID string `json:"account_id"`
	// StepID is the step that was rendered.
	StepID string `json:"step_id"`
	// ContactID echoes the contact rendered for, when one was given.
	ContactID string `json:"contact_id,omitempty"`
}

TestEmailResult confirms a test send.

type TimeInterval

type TimeInterval struct {
	Start int `json:"start"`
	End   int `json:"end"`
}

TimeInterval is one sending window inside a day, in minutes since local midnight. End is exclusive and must be greater than Start and at most 1440.

type TimelineEvent

type TimelineEvent struct {
	// Type is one of the Timeline* constants.
	Type string    `json:"type"`
	At   time.Time `json:"at"`

	EmailAccountID    *string `json:"email_account_id,omitempty"`
	EmailAccountEmail *string `json:"email_account_email,omitempty"`
	EmailAccountName  *string `json:"email_account_name,omitempty"`

	CampaignID   *string `json:"campaign_id,omitempty"`
	CampaignName *string `json:"campaign_name,omitempty"`
	StepID       *string `json:"step_id,omitempty"`
	StepName     *string `json:"step_name,omitempty"`

	TaskID *string `json:"task_id,omitempty"`
	// Subject is the email subject, or the page title for [TimelinePageHit].
	Subject *string `json:"subject,omitempty"`

	Reason *string `json:"reason,omitempty"`
	// Source is the suppression source on [TimelineSuppressed] events and the
	// first-touch ContactSource* value on [TimelineContactCreated] events.
	Source   *string `json:"source,omitempty"`
	Provider *string `json:"provider,omitempty"`
	// Intent is the reply-intent classification, for example "positive".
	Intent *string `json:"intent,omitempty"`
	// Content is the note body for [TimelineNote] events.
	Content *string `json:"content,omitempty"`

	// ScheduledFor, JoinURL and MeetingState are set on meeting events.
	ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
	JoinURL      *string    `json:"join_url,omitempty"`
	MeetingState *string    `json:"meeting_state,omitempty"`

	// CategoryID and CategoryTitle are set on category lifecycle events;
	// SourceDetail on [TimelineContactCreated] (the file, campaign, sheet,
	// form, automation or API key name).
	CategoryID    *string `json:"category_id,omitempty"`
	CategoryTitle *string `json:"category_title,omitempty"`
	SourceDetail  *string `json:"source_detail,omitempty"`

	// FormID and FormName are set on [TimelineFormSubmitted] events.
	FormID   *string `json:"form_id,omitempty"`
	FormName *string `json:"form_name,omitempty"`

	// PageHit is the full page view behind a [TimelinePageHit] event.
	PageHit *ContactPageHit `json:"page_hit,omitempty"`

	// Machine is set on opens and clicks: true when an automated fetcher did
	// it rather than the person. MachineReason is one of the MachineReason*
	// constants when the event was logged individually. Automated clicks stay
	// on the feed for the record but never count the step as clicked.
	Machine       *bool   `json:"machine,omitempty"`
	MachineReason *string `json:"machine_reason,omitempty"`

	// Link is the exact link behind an [TimelineEmailClicked] event.
	Link *ContactLinkClick `json:"link,omitempty"`

	// Origin is the mail client, device and rough location of an open or
	// click, when the event was logged individually. Opens appear once per
	// event, so a contact who opened from two devices has two rows.
	Origin *EngagementOrigin `json:"origin,omitempty"`

	// UserID is the author of a note or the member behind a lifecycle event.
	UserID *string `json:"user_id,omitempty"`
}

TimelineEvent is one entry in a contact's merged activity feed. Which optional fields are populated depends on Type.

type TimelineParams added in v0.3.0

type TimelineParams struct {
	// Limit is 1 to 200 (default 50). Anything else is a 400.
	ListOptions
	// Before is deprecated: it returns events strictly older than the
	// timestamp and can skip events sharing an instant with the page
	// boundary. It is ignored when Cursor is set. Prefer paging by cursor.
	Before *time.Time `json:"-"`
}

TimelineParams paginates ContactService.ListTimeline.

type Timezone

type Timezone struct {
	// Name is the IANA identifier, for example "Europe/Berlin".
	Name string `json:"name"`
	// DisplayName is how to label it in a picker.
	DisplayName string `json:"display_name"`
}

Timezone is one selectable IANA timezone.

type Token

type Token struct {
	// AccessToken is the bearer token used to authenticate requests.
	AccessToken string `json:"access_token"`
	// TokenType is the token type; the API returns "Bearer".
	TokenType string `json:"token_type"`
	// RefreshToken, when present, can be exchanged for a fresh access token.
	RefreshToken string `json:"refresh_token,omitempty"`
	// Scope is the space-delimited set of granted scopes.
	Scope string `json:"scope,omitempty"`
	// ExpiresIn is the lifetime in seconds as returned by the server. It is
	// used to compute Expiry and is not otherwise meaningful afterwards.
	ExpiresIn int64 `json:"expires_in,omitempty"`
	// Expiry is the absolute time at which AccessToken expires. A zero value
	// means the token never expires (or expiry is unknown).
	Expiry time.Time `json:"-"`
}

Token is an OAuth 2.1 token as returned by the Warmbly token endpoint.

func (*Token) Type

func (t *Token) Type() string

Type returns the token's HTTP Authorization scheme, defaulting to "Bearer".

func (*Token) Valid

func (t *Token) Valid() bool

Valid reports whether the token is non-empty and not (about to be) expired.

type TokenSource

type TokenSource interface {
	// Token returns a valid token or an error.
	Token() (*Token, error)
}

TokenSource yields valid tokens on demand, refreshing transparently. It is the same contract as golang.org/x/oauth2.TokenSource, so existing sources interoperate via a thin adapter.

func StaticTokenSource

func StaticTokenSource(tok *Token) TokenSource

StaticTokenSource returns a TokenSource that always yields tok. It never refreshes and is mainly useful in tests or when a token is injected from the environment.

type TrackingDomainStatus

type TrackingDomainStatus struct {
	TrackingDomain           string     `json:"tracking_domain"`
	TrackingDomainVerified   bool       `json:"tracking_domain_verified"`
	TrackingDomainVerifiedAt *time.Time `json:"tracking_domain_verified_at"`

	// CNAMETarget is the value to put in the CNAME record: this install's
	// tracking host. Empty when the deployment has no tracking host.
	CNAMETarget string `json:"cname_target"`
	// Status is one of the TrackingStatus* constants.
	Status string `json:"status"`
	// Message explains Status in one sentence and is safe to show as-is.
	Message string `json:"message"`
	// Observed is what DNS actually returned, when it differs from the target.
	Observed string `json:"observed,omitempty"`
	// TrackingHostUnresolvable reports that the record is correct but this
	// install's own tracking host has no DNS record, so nothing will be
	// recorded. That is an operator fault, not a caller one.
	TrackingHostUnresolvable bool `json:"tracking_host_unresolvable"`
}

TrackingDomainStatus is the state of a mailbox's custom click/open tracking domain. Verified turns true once the customer's subdomain CNAMEs to this install's tracking host. Until then opens and clicks go through the shared host, so an unverified domain never blocks sending.

Everything below TrackingDomainVerifiedAt is diagnostic, so a "pending" badge always comes with something to act on.

type TrialStatus

type TrialStatus struct {
	IsInTrial     bool       `json:"is_in_trial"`
	TrialEndsAt   *time.Time `json:"trial_ends_at,omitempty"`
	DaysRemaining int        `json:"days_remaining"`
	IsExpired     bool       `json:"is_expired"`
	IsSubscribed  bool       `json:"is_subscribed"`
}

TrialStatus is where the workspace stands in its free trial.

type TwoFAEnrollment

type TwoFAEnrollment struct {
	// Secret is the shared TOTP secret, and OTPAuthURI the same thing as a
	// scannable otpauth:// URL.
	Secret     string `json:"secret"`
	OTPAuthURI string `json:"otpauth_uri"`
}

TwoFAEnrollment is what a new authenticator needs to be set up.

type TwoFARecoveryCodes

type TwoFARecoveryCodes struct {
	RecoveryCodes []string `json:"recovery_codes"`
}

TwoFARecoveryCodes are the single-use codes that get an account back when the authenticator is lost. They are shown once.

type TwoFAStatus

type TwoFAStatus struct {
	Enabled bool `json:"enabled"`
}

TwoFAStatus reports whether the account has two-factor enabled.

type UniboxComposeParams

type UniboxComposeParams struct {
	// EmailAccountID picks the sending mailbox. Leave it empty (or set it to
	// "auto") to let the server choose the best mailbox for the first
	// recipient; see [UniboxService.ComposeCandidates].
	EmailAccountID string `json:"email_account_id,omitempty"`
	// FromTagID scopes an automatic pick to mailboxes carrying that tag. It is
	// ignored when EmailAccountID names a mailbox.
	FromTagID string   `json:"from_tag_id,omitempty"`
	To        []string `json:"to"`
	CC        []string `json:"cc,omitempty"`
	BCC       []string `json:"bcc,omitempty"`
	Subject   string   `json:"subject"`
	BodyHTML  string   `json:"body_html,omitempty"`
	BodyPlain string   `json:"body_plain,omitempty"`
	// SendMode is [SendModeInstant] (the default), [SendModeSmart] or
	// [SendModeScheduled].
	SendMode    string     `json:"send_mode,omitempty"`
	ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
}

UniboxComposeParams sends a brand-new outbound email. Unlike a reply it is checked against the workspace suppression list before being queued: a suppressed recipient anywhere in To, CC or BCC fails the whole call with a 400.

type UniboxFolderCount added in v0.3.0

type UniboxFolderCount struct {
	// Folder is one of the Folder* constants.
	Folder string `json:"folder"`
	Unread int    `json:"unread"`
	Total  int    `json:"total"`
}

UniboxFolderCount is one canonical folder's thread counts.

type UniboxLabelCount

type UniboxLabelCount struct {
	ID     string `json:"id"`
	Title  string `json:"title"`
	Color  string `json:"color,omitempty"`
	Unread int    `json:"unread"`
	Total  int    `json:"total"`
}

UniboxLabelCount is one tag's or label's share of the inbox.

type UniboxListParams

type UniboxListParams struct {
	ListOptions
	// From and Subject are substring filters.
	From    string
	Subject string
	// Address matches conversations with one person in either direction: mail
	// they sent, and mail sent to them.
	Address string
	// Direction is [DirectionSent] or [DirectionReceived]. Empty returns both.
	Direction string
	// Folder narrows to one canonical folder (a Folder* constant). Empty
	// searches every folder except spam and trash; an unknown value is a 400.
	Folder string
	// Unseen restricts to threads with unread messages.
	Unseen *bool
	// AwaitingReply restricts to threads whose latest message you sent.
	AwaitingReply *bool
	// AgentDrafts restricts to threads with a pending inbox-agent draft.
	AgentDrafts *bool
	// Uncategorized restricts to threads carrying no conversation labels.
	Uncategorized *bool
	// Snoozed set to true returns only snoozed threads. Leave it nil to
	// exclude snoozed threads, which is the default; the server ignores an
	// explicit false. Set SnoozedAny to include snoozed and unsnoozed threads
	// alike.
	Snoozed    *bool
	SnoozedAny bool
	// Since and Until bound the thread date.
	Since time.Time
	Until time.Time
	// EmailID restricts to one mailbox; EmailIDs matches any of several.
	EmailID  string
	EmailIDs []string
	// CategoryIDs matches threads carrying any of the given conversation
	// labels.
	CategoryIDs []string
}

UniboxListParams filters and paginates the unified inbox.

type UniboxMailboxCount

type UniboxMailboxCount struct {
	ID     string `json:"id"`
	Email  string `json:"email"`
	Name   string `json:"name,omitempty"`
	Unread int    `json:"unread"`
	Total  int    `json:"total"`
}

UniboxMailboxCount is one mailbox's share of the inbox.

type UniboxMessage

type UniboxMessage struct {
	ID      string `json:"id"`
	EmailID string `json:"email_id"`
	// Mailbox is the provider-side folder id.
	Mailbox int `json:"mailbox,omitempty"`
	// Folder is the canonical folder the message is filed in: one of the
	// Folder* constants. Empty on mail synced before folder tracking.
	Folder    string   `json:"folder,omitempty"`
	ThreadID  string   `json:"thread_id"`
	MessageID string   `json:"message_id,omitempty"`
	GmailID   string   `json:"gmail_id,omitempty"`
	ParentID  string   `json:"parent_id,omitempty"`
	UID       int      `json:"uid,omitempty"`
	ModSeq    int      `json:"mod_seq,omitempty"`
	Flags     []string `json:"flags,omitempty"`

	BCC       []string `json:"bcc,omitempty"`
	CC        []string `json:"cc,omitempty"`
	FromAddr  []string `json:"from_addr,omitempty"`
	InReplyTo []string `json:"in_reply_to,omitempty"`
	ReplyTo   []string `json:"reply_to,omitempty"`
	ToAddr    []string `json:"to_addr,omitempty"`

	Subject      string    `json:"subject"`
	Size         int       `json:"size,omitempty"`
	InternalDate time.Time `json:"internal_date"`
	SentDate     time.Time `json:"sent_date,omitempty"`
	Snippet      string    `json:"snippet,omitempty"`
	Seen         bool      `json:"seen"`

	// BodyPlain is the plain-text body. BodyHTML is sanitized for display:
	// scripts, event handlers and unsafe URL schemes are stripped before it
	// leaves the API.
	BodyPlain string `json:"body_plain,omitempty"`
	BodyHTML  string `json:"body_html,omitempty"`
	// BodyTruncated is true when the stored body could not be read, in which
	// case BodyPlain holds only the preview snippet. Show a notice rather than
	// presenting the partial text as the whole message.
	BodyTruncated bool `json:"body_truncated,omitempty"`

	UpdatedAt time.Time `json:"updated_at,omitempty"`
	CreatedAt time.Time `json:"created_at,omitempty"`
}

UniboxMessage is a single message, as returned by UniboxService.Get and by the rows of UniboxService.Thread.

The two endpoints spell the envelope differently on the wire (the single read uses "from", "to" and "date"; thread rows use "from_addr", "to_addr" and "sent_date"). The SDK accepts both and exposes one shape. Bodies are only present on a single read; thread rows carry the snippet.

func (*UniboxMessage) UnmarshalJSON added in v0.3.0

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

UnmarshalJSON accepts both envelope spellings the API uses for a message; see the type comment.

type UniboxOverview

type UniboxOverview struct {
	Total         int `json:"total"`
	Unread        int `json:"unread"`
	Today         int `json:"today"`
	Week          int `json:"week"`
	Snoozed       int `json:"snoozed"`
	AwaitingReply int `json:"awaiting_reply"`
	// AwaitingAgentDraft counts threads with a pending inbox-agent draft
	// waiting for review; see [UniboxService.AgentDrafts].
	AwaitingAgentDraft int `json:"awaiting_agent_draft"`
	// ScheduledPending is how many sends are queued but not yet out, capped
	// for display at ScheduledPendingMax.
	ScheduledPending    int `json:"scheduled_pending"`
	ScheduledPendingMax int `json:"scheduled_pending_max,omitempty"`

	// Folders always lists all six canonical folders, zero-filled, in sidebar
	// order.
	Folders    []UniboxFolderCount  `json:"folders,omitempty"`
	Mailboxes  []UniboxMailboxCount `json:"mailboxes,omitempty"`
	Tags       []UniboxLabelCount   `json:"tags,omitempty"`
	Categories []UniboxLabelCount   `json:"categories,omitempty"`

	GeneratedAt      time.Time `json:"generated_at,omitempty"`
	WindowTodayStart time.Time `json:"window_today_start,omitempty"`
	WindowWeekStart  time.Time `json:"window_week_start,omitempty"`
}

UniboxOverview is the inbox sidebar: totals across the workspace, and per folder, per mailbox and per label.

type UniboxReplyParams

type UniboxReplyParams struct {
	EmailAccountID string   `json:"email_account_id"`
	To             []string `json:"to"`
	CC             []string `json:"cc,omitempty"`
	BCC            []string `json:"bcc,omitempty"`
	Subject        string   `json:"subject"`
	BodyHTML       string   `json:"body_html,omitempty"`
	BodyPlain      string   `json:"body_plain,omitempty"`
	// InReplyTo holds the Message-IDs being replied to, and ThreadID keeps the
	// message in the provider's conversation. When InReplyTo is empty and
	// ThreadID is set, the server fills InReplyTo with the thread's latest
	// Message-ID so the reply nests in the recipient's client too.
	InReplyTo []string `json:"in_reply_to,omitempty"`
	ThreadID  string   `json:"thread_id,omitempty"`
	// SendMode is [SendModeInstant] (the default), [SendModeSmart] or
	// [SendModeScheduled].
	SendMode    string     `json:"send_mode,omitempty"`
	ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
}

UniboxReplyParams sends a reply from a mailbox into an existing thread.

type UniboxService

type UniboxService service

UniboxService is the unified inbox: every connected mailbox's mail in one place, plus replying, composing, snoozing, conversation labels, scheduled sends, autosaved drafts and the AI drafting surfaces.

List rows are previews: they carry a snippet, never the body. Read a single message with UniboxService.Get to receive its plain-text body and a sanitized HTML body.

func (*UniboxService) AgentDrafts

func (s *UniboxService) AgentDrafts(ctx context.Context, opts ...RequestOption) ([]AgentDraft, *Response, error)

AgentDrafts returns the workspace's pending inbox-agent drafts, newest first. The agent only runs when the workspace has opted in via Organization.InboxAgentEnabled.

func (*UniboxService) ApproveAgentDraft

func (s *UniboxService) ApproveAgentDraft(ctx context.Context, id, body string, opts ...RequestOption) (*SendResult, *Response, error)

ApproveAgentDraft sends an agent draft through the normal reply path, optionally replacing its body first. Approval is claimed before the send, so two concurrent approvals can never double-send; the loser gets a 409. If the send itself fails the draft returns to pending so it can be retried.

func (*UniboxService) CancelScheduled

func (s *UniboxService) CancelScheduled(ctx context.Context, taskID string, opts ...RequestOption) (*Response, error)

CancelScheduled cancels a queued send. The cancellation is recorded immediately and the task short-circuits when its turn comes.

func (*UniboxService) Compose

Compose sends a brand-new outbound email and reports which mailbox sent it.

func (*UniboxService) ComposeCandidates

func (s *UniboxService) ComposeCandidates(ctx context.Context, to string, opts ...RequestOption) (*ComposeCandidates, *Response, error)

ComposeCandidates scores the workspace's mailboxes as senders for one recipient, so a picker can explain the automatic choice.

func (*UniboxService) DeleteDraft

func (s *UniboxService) DeleteDraft(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DeleteDraft discards an autosaved compose draft.

func (*UniboxService) DiscardAgentDraft

func (s *UniboxService) DiscardAgentDraft(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

DiscardAgentDraft throws an agent draft away unsent.

func (*UniboxService) DraftCompose

func (s *UniboxService) DraftCompose(ctx context.Context, to, subject, instruction string, opts ...RequestOption) (*AIDraft, *Response, error)

DraftCompose writes a new email grounded in the contact record, prior correspondence and the workspace voice profile. It spends AI credits and never sends. When the request gives it too little to work with it returns a clarifying AIDraft.Question instead of a draft.

func (*UniboxService) DraftReply

func (s *UniboxService) DraftReply(ctx context.Context, threadID, instruction string, opts ...RequestOption) (*AIDraft, *Response, error)

DraftReply writes a reply grounded in the thread's stored message text. It spends AI credits and never sends.

func (*UniboxService) Drafts

func (s *UniboxService) Drafts(ctx context.Context, opts ...RequestOption) ([]ComposeDraft, *Response, error)

Drafts returns the caller's autosaved compose drafts.

func (*UniboxService) Get

Get returns a single message by id, including its plain-text body and a sanitized HTML body. Check UniboxMessage.BodyTruncated before treating BodyPlain as the whole message.

func (*UniboxService) List

func (s *UniboxService) List(ctx context.Context, params *UniboxListParams, opts ...RequestOption) (*Page[UniboxThread], error)

List returns a page of inbox threads, newest first. Rows are previews; see UniboxService.Get for bodies.

func (*UniboxService) MarkFolderSeen added in v0.3.0

func (s *UniboxService) MarkFolderSeen(ctx context.Context, folder string, seen bool, opts ...RequestOption) (*Response, error)

MarkFolderSeen marks every message in one canonical folder (a Folder* constant) read or unread, workspace-wide. A folder sweep and an id list are different requests: the server rejects a body that carries both, which is why this is a separate method from UniboxService.MarkSeen.

func (*UniboxService) MarkSeen

func (s *UniboxService) MarkSeen(ctx context.Context, emailIDs []string, seen bool, opts ...RequestOption) (*Response, error)

MarkSeen marks up to 500 messages read or unread by id. To sweep a whole folder use UniboxService.MarkFolderSeen.

func (*UniboxService) Overview

func (s *UniboxService) Overview(ctx context.Context, opts ...RequestOption) (*UniboxOverview, *Response, error)

Overview returns the inbox sidebar counts.

func (*UniboxService) Reply

func (s *UniboxService) Reply(ctx context.Context, params *UniboxReplyParams, opts ...RequestOption) (*SendResult, *Response, error)

Reply sends a reply into an existing thread.

func (*UniboxService) SaveDraft

func (s *UniboxService) SaveDraft(ctx context.Context, id string, params *ComposeDraftParams, opts ...RequestOption) (*Response, error)

SaveDraft creates or replaces an autosaved compose draft. The id is client-generated, so repeating the call with the same id is an update rather than a new draft.

func (*UniboxService) Scheduled

func (s *UniboxService) Scheduled(ctx context.Context, threadID string, opts ...RequestOption) ([]ScheduledSend, *Response, error)

Scheduled returns queued sends that have not left yet. Pass a threadID to narrow it to one conversation.

func (*UniboxService) SetThreadLabels

func (s *UniboxService) SetThreadLabels(ctx context.Context, threadID string, categoryIDs []string, opts ...RequestOption) ([]MiniCategory, *Response, error)

SetThreadLabels replaces a thread's conversation labels wholesale, which makes it idempotent. Labels are contact categories; manage them with GroupService.

func (*UniboxService) Snooze

func (s *UniboxService) Snooze(ctx context.Context, threadID string, until time.Time, opts ...RequestOption) (*UniboxSnooze, *Response, error)

Snooze hides a thread until the given time. Snoozing an already snoozed thread moves its wake-up time.

func (*UniboxService) Snoozes

func (s *UniboxService) Snoozes(ctx context.Context, opts ...RequestOption) ([]UniboxSnooze, *Response, error)

Snoozes returns the caller's active snoozes.

func (*UniboxService) Thread

func (s *UniboxService) Thread(ctx context.Context, threadID, emailID string, params *ListOptions, opts ...RequestOption) (*Page[UniboxMessage], error)

Thread returns a page of the messages in one conversation, oldest first. Pass an empty emailID to search every mailbox. Rows carry the snippet, not the body.

func (*UniboxService) ThreadLabels

func (s *UniboxService) ThreadLabels(ctx context.Context, threadID string, opts ...RequestOption) ([]MiniCategory, *Response, error)

ThreadLabels returns the conversation labels on a thread.

func (*UniboxService) UnseenCount

func (s *UniboxService) UnseenCount(ctx context.Context, emailID string, opts ...RequestOption) (int, *Response, error)

UnseenCount returns how many unread messages there are, across the workspace or in a single mailbox when emailID is set.

func (*UniboxService) Unsnooze

func (s *UniboxService) Unsnooze(ctx context.Context, threadID string, opts ...RequestOption) (*Response, error)

Unsnooze returns a snoozed thread to the inbox immediately. It is idempotent: unsnoozing a thread that is not snoozed still succeeds.

type UniboxSnooze

type UniboxSnooze struct {
	ID           string    `json:"id"`
	UserID       string    `json:"user_id"`
	ThreadID     string    `json:"thread_id"`
	SnoozedUntil time.Time `json:"snoozed_until"`
	CreatedAt    time.Time `json:"created_at,omitempty"`
	UpdatedAt    time.Time `json:"updated_at,omitempty"`
}

UniboxSnooze hides a thread until SnoozedUntil, at which point it returns to the inbox.

type UniboxThread

type UniboxThread struct {
	ID       string   `json:"id"`
	EmailID  string   `json:"email_id"`
	ThreadID string   `json:"thread_id"`
	FromAddr []string `json:"from_addr"`
	ToAddr   []string `json:"to_addr"`
	Subject  string   `json:"subject"`
	Snippet  string   `json:"snippet"`
	// InternalDate is the provider's timestamp for the newest message.
	InternalDate time.Time `json:"internal_date"`
	Seen         bool      `json:"seen"`
	MessageCount int       `json:"message_count"`
	HasUnread    bool      `json:"has_unread"`
	// Labels are the conversation labels on the thread. They share the
	// category registry with contact tags.
	Labels []MiniCategory `json:"labels,omitempty"`
}

UniboxThread is one conversation in the unified inbox list. It is a preview row: Snippet is the only body text it carries.

type UnsubscribeSettings added in v0.3.0

type UnsubscribeSettings struct {
	// Mode is [UnsubscribeModeText], [UnsubscribeModeLink] or
	// [UnsubscribeModeOff].
	Mode string `json:"mode"`
	// Text is the sentence appended in text mode. Default: "If this isn't
	// relevant, just reply and let me know and I won't email you again."
	Text string `json:"text"`
	// LinkIntro and LinkText make up the link-mode line, rendered as
	// "<intro> <a>text</a>". Defaults: "Not the right person, or not
	// interested?" and "Unsubscribe".
	LinkIntro string `json:"link_intro"`
	LinkText  string `json:"link_text"`
}

UnsubscribeSettings is the organization default for the opt-out appended after the signature of every campaign email. Copy fields are single lines of at most 300 characters; whitespace is collapsed and longer text is cut. Blank copy falls back to the server defaults at send time.

type UpdateMemberParams

type UpdateMemberParams struct {
	RoleIDs []string `json:"role_ids,omitempty"`
	RoleID  *string  `json:"role_id,omitempty"`
}

UpdateMemberParams replaces a member's assigned roles.

type UpsertContactConfig added in v0.3.0

type UpsertContactConfig struct {
	Email     string `json:"email"`
	FirstName string `json:"first_name,omitempty"`
	LastName  string `json:"last_name,omitempty"`
	Company   string `json:"company,omitempty"`
	Phone     string `json:"phone,omitempty"`
	// CustomFields are written into the contact's custom fields. A field
	// whose value renders empty is left out rather than cleared.
	CustomFields []TemplateField `json:"custom_fields,omitempty"`
	// CategoryIDs tags the contact and SegmentIDs pins it into segments. Ids
	// are validated on save.
	CategoryIDs []string `json:"category_ids,omitempty"`
	SegmentIDs  []string `json:"segment_ids,omitempty"`
	// CampaignID, when set, enrolls the contact in that campaign.
	CampaignID string `json:"campaign_id,omitempty"`
	// IfExists is [IfExistsUpdate] (the default) or [IfExistsSkip].
	IfExists string `json:"if_exists,omitempty"`
}

UpsertContactConfig is the config of an ActionUpsertContact node. Every contact field is a Go template rendered against the event data. Email is required and is validated when the automation is saved; a rendered email that is empty or has no "@" fails the node at run time.

func (*UpsertContactConfig) Config added in v0.3.0

func (c *UpsertContactConfig) Config() (json.RawMessage, error)

Config renders the struct as an AutomationNode.Config value.

type UsageOverview

type UsageOverview struct {
	UserID string `json:"user_id"`
	// Period is [UsagePeriodDay], [UsagePeriodWeek] or [UsagePeriodMonth].
	Period        string            `json:"period"`
	EmailAccounts EmailAccountUsage `json:"email_accounts"`
	Campaigns     CampaignUsage     `json:"campaigns"`
	Contacts      ContactUsage      `json:"contacts"`
	API           APIUsage          `json:"api"`
}

UsageOverview is the organization's consumption against its plan.

type User

type User struct {
	ID        string   `json:"id"`
	FirstName string   `json:"first_name"`
	LastName  string   `json:"last_name"`
	Email     string   `json:"email"`
	AvatarURL *string  `json:"avatar_url,omitempty"`
	Roles     []string `json:"roles,omitempty"`

	ReferralSource        *string    `json:"referral_source,omitempty"`
	OnboardingCompletedAt *time.Time `json:"onboarding_completed_at,omitempty"`

	MaxOrganizations int  `json:"max_organizations,omitempty"`
	FreeTrialUsed    bool `json:"free_trial_used,omitempty"`

	// UndoSendSeconds is how long an instant send is held before it leaves, so
	// the user can still cancel it.
	UndoSendSeconds int `json:"undo_send_seconds,omitempty"`

	// IsAdmin reports platform-admin access; AdminPermissions is the raw
	// bitmask behind it. Both are only populated on the caller's own profile.
	AdminPermissions uint64 `json:"admin_permissions,omitempty"`
	IsAdmin          bool   `json:"is_admin,omitempty"`

	// DeletionScheduledFor is set while the account is pending a hard delete.
	DeletionScheduledAt  *time.Time `json:"deletion_scheduled_at,omitempty"`
	DeletionScheduledFor *time.Time `json:"deletion_scheduled_for,omitempty"`

	// Folders, Tags and Categories are the user's label groups, returned on
	// their own profile.
	Folders    []Group `json:"folders,omitempty"`
	Tags       []Group `json:"tags,omitempty"`
	Categories []Group `json:"categories,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

User is a Warmbly user account.

func (*User) PendingDeletion

func (u *User) PendingDeletion() bool

PendingDeletion reports whether the account is scheduled for a hard delete.

type UserSession

type UserSession struct {
	ID string `json:"id"`
	// Current marks the session making the request.
	Current bool   `json:"current"`
	Browser string `json:"browser,omitempty"`
	OS      string `json:"os,omitempty"`

	LocationCity    string `json:"location_city,omitempty"`
	LocationRegion  string `json:"location_region,omitempty"`
	LocationCountry string `json:"location_country,omitempty"`
	CountryCode     string `json:"country_code,omitempty"`

	// AuthProvider is how the session was created, for example "password",
	// "passkey", "apple" or "google".
	AuthProvider string    `json:"auth_provider"`
	CreatedAt    time.Time `json:"created_at"`
	LastActiveAt time.Time `json:"last_active_at"`
}

UserSession is one active session on the account.

type VerifyResult

type VerifyResult struct {
	Email string `json:"email"`
	// Status is [VerifyStatusValid], [VerifyStatusRisky],
	// [VerifyStatusInvalid] or [VerifyStatusUnknown]. Unknown covers
	// greylisting, timeouts and blocked outbound port 25; it is never a reason
	// to drop an address.
	Status string `json:"status"`
	// SubStatus refines Status with one of the VerifySubStatus* constants, or
	// "" when nothing more specific is known.
	SubStatus  string `json:"sub_status,omitempty"`
	Reason     string `json:"reason"`
	IsCatchAll bool   `json:"is_catch_all"`
	HasMX      bool   `json:"has_mx"`
	// Provider is who produced the verdict: "builtin" for the in-house probe,
	// or the workspace's connected verification provider.
	Provider string `json:"provider,omitempty"`
	// Confidence is the scored certainty of Status, when the verifier
	// provides one.
	Confidence int       `json:"confidence,omitempty"`
	CheckedAt  time.Time `json:"checked_at"`
}

VerifyResult is the outcome of verifying a single recipient address.

type WarmupAnalytics

type WarmupAnalytics struct {
	EmailAccountID string            `json:"email_account_id"`
	Email          string            `json:"email"`
	DateRange      DateRange         `json:"date_range"`
	Summary        WarmupSummary     `json:"summary"`
	DailyStats     []WarmupDailyStat `json:"daily_stats"`
}

WarmupAnalytics is warmup progress for one mailbox, or for the organization when no mailbox was named.

type WarmupAppealParams

type WarmupAppealParams struct {
	Reason string `json:"reason"`
}

WarmupAppealParams are the parameters for appealing a warmup ban.

type WarmupAppealResult

type WarmupAppealResult struct {
	AppealID string `json:"appeal_id"`
}

WarmupAppealResult identifies the appeal that was filed.

type WarmupBanStatus

type WarmupBanStatus struct {
	EmailAccountID string `json:"email_account_id"`
	Blocked        bool   `json:"blocked"`
	// HealthState is the mailbox's warmup health: "healthy", "watch",
	// "throttled", "quarantined" or "blocked". Health is judged only on what
	// happened while the mailbox was in the pool.
	HealthState string `json:"health_state"`
	// Reason, BlockedAt and BlockedUntil are omitted when the mailbox is not
	// blocked.
	Reason       string     `json:"reason,omitempty"`
	BlockedAt    *time.Time `json:"blocked_at,omitempty"`
	BlockedUntil *time.Time `json:"blocked_until,omitempty"`
	// CanAppeal reports whether [EmailService.AppealWarmupBan] is open to the
	// owner; PendingAppeal reports that one has already been filed.
	CanAppeal     bool `json:"can_appeal"`
	PendingAppeal bool `json:"pending_appeal"`
}

WarmupBanStatus describes whether a mailbox has been blocked from the warmup pool and whether that block can be appealed.

type WarmupDailyStat

type WarmupDailyStat struct {
	Date          string `json:"date"`
	EmailsSent    int64  `json:"emails_sent"`
	EmailsReplied int64  `json:"emails_replied"`
	TargetVolume  int64  `json:"target_volume"`
}

WarmupDailyStat is one day of warmup volume against its target.

type WarmupDomainPlacement added in v0.3.0

type WarmupDomainPlacement struct {
	Provider  string  `json:"provider"`
	Domain    string  `json:"domain"`
	Delivered int64   `json:"delivered"`
	Spam      int64   `json:"spam"`
	InboxRate float64 `json:"inbox_rate"`
	SpamRate  float64 `json:"spam_rate"`
}

WarmupDomainPlacement is one recipient domain's warmup placement rollup. Delivered counts verified warmup arrivals; Spam the ones flagged into junk.

type WarmupHealth added in v0.3.0

type WarmupHealth struct {
	// State is one of the Band* constants.
	State string `json:"state"`
	// Score runs 0 to 100, higher being healthier.
	Score  float64 `json:"score"`
	Reason string  `json:"reason,omitempty"`
	// SpamScore is the last content spam score, 0 to 100, lower being safer.
	SpamScore    int        `json:"spam_score"`
	BlockedUntil *time.Time `json:"blocked_until,omitempty"`
	EvaluatedAt  *time.Time `json:"evaluated_at,omitempty"`
}

WarmupHealth is a mailbox's standing in the warmup pool.

type WarmupRampHold added in v0.3.0

type WarmupRampHold struct {
	// Placements is how many warmup messages landed in spam in the last 48
	// hours, out of Sends.
	Placements int  `json:"placements"`
	Sends      int  `json:"sends"`
	VolumeCut  bool `json:"volume_cut"`
	// ResumesAt is when the ramp climbs again if nothing else lands in spam.
	ResumesAt time.Time `json:"resumes_at"`
}

WarmupRampHold explains a frozen warmup ramp. It is present for the whole freeze; VolumeCut says whether today's volume is also reduced, which lasts a shorter window.

type WarmupRoutingRule

type WarmupRoutingRule struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	Name           string `json:"name"`
	// Priority orders evaluation; lower runs first.
	Priority int `json:"priority"`
	// SenderMatchType is one of the Match* constants; SenderMatchValue is
	// ignored for [MatchAny].
	SenderMatchType  string `json:"sender_match_type"`
	SenderMatchValue string `json:"sender_match_value,omitempty"`
	// RecipientMatchType and RecipientMatchValue mirror the sender side.
	RecipientMatchType  string `json:"recipient_match_type"`
	RecipientMatchValue string `json:"recipient_match_value,omitempty"`
	// Weight scales how strongly a match is preferred.
	Weight  float64 `json:"weight"`
	Enabled bool    `json:"enabled"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

WarmupRoutingRule biases partner selection in the warmup pool.

type WarmupRoutingRuleParams

type WarmupRoutingRuleParams struct {
	Name     string `json:"name"`
	Priority int    `json:"priority,omitempty"`
	// SenderMatchType and RecipientMatchType are Match* constants.
	SenderMatchType     string  `json:"sender_match_type"`
	SenderMatchValue    string  `json:"sender_match_value,omitempty"`
	RecipientMatchType  string  `json:"recipient_match_type"`
	RecipientMatchValue string  `json:"recipient_match_value,omitempty"`
	Weight              float64 `json:"weight,omitempty"`
	Enabled             *bool   `json:"enabled,omitempty"`
}

WarmupRoutingRuleParams creates or replaces a routing rule. The API replaces the rule wholesale on update, so send the complete desired state.

type WarmupRoutingService

type WarmupRoutingService service

WarmupRoutingService manages warmup routing rules: preferences for how the premium partner pool pairs senders with recipients, for example "send to Gmail recipients only from Google-hosted mailboxes".

Rules are evaluated in Priority order and bias partner selection by Weight. They are preferences, not hard constraints: the pool falls back to an unmatched partner rather than sending nothing.

func (*WarmupRoutingService) Create

Create adds a routing rule.

func (*WarmupRoutingService) Delete

func (s *WarmupRoutingService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete removes a routing rule.

func (*WarmupRoutingService) List

List returns the workspace's routing rules in priority order.

func (*WarmupRoutingService) Update

Update replaces a routing rule.

type WarmupStatus added in v0.3.0

type WarmupStatus struct {
	Enabled   bool       `json:"enabled"`
	Paused    bool       `json:"paused"`
	PausedAt  *time.Time `json:"paused_at,omitempty"`
	StartedAt time.Time  `json:"started_at"`
	// CurrentVolume is today's warmup sends so far; TargetVolume is today's
	// target after any hold; MaxVolume is the configured ceiling.
	CurrentVolume int `json:"current_volume"`
	TargetVolume  int `json:"target_volume"`
	MaxVolume     int `json:"max_volume"`
	// ReplyRate is the configured warmup reply percentage.
	ReplyRate  int `json:"reply_rate"`
	DaysActive int `json:"days_active"`
	// RampHold explains a ramp that is not climbing, so a TargetVolume below
	// the plain ramp is never an unexplained drop. Nil while the ramp is free
	// to climb.
	RampHold *WarmupRampHold `json:"ramp_hold,omitempty"`
}

WarmupStatus is a mailbox's warmup ramp as the scheduler will act on it.

type WarmupSummary

type WarmupSummary struct {
	TotalSent    int64   `json:"total_sent"`
	TotalReplied int64   `json:"total_replied"`
	AverageDaily float64 `json:"average_daily"`
	ReplyRate    float64 `json:"reply_rate"`
	// TargetProgress is how far the ramp has come, from 0 to 1.
	TargetProgress float64 `json:"target_progress"`
	DaysActive     int     `json:"days_active"`
}

WarmupSummary is the rollup for a warmup window.

type Webhook

type Webhook struct {
	ID             string `json:"id"`
	OrganizationID string `json:"organization_id"`
	// URL is the HTTPS endpoint that receives deliveries.
	URL         string `json:"url"`
	Description string `json:"description"`
	// EventTypes are the event keys this endpoint subscribes to. An empty list
	// means every non-firehose event.
	EventTypes []string `json:"event_types"`
	// Enabled reports whether deliveries are being attempted.
	Enabled bool `json:"enabled"`

	LastSuccessAt     *time.Time `json:"last_success_at,omitempty"`
	LastFailureAt     *time.Time `json:"last_failure_at,omitempty"`
	LastFailureReason *string    `json:"last_failure_reason,omitempty"`
	// ConsecutiveFailures drives the auto-disable circuit breaker.
	ConsecutiveFailures int `json:"consecutive_failures"`

	// OAuthApplicationID is set for app-scoped endpoints, whose URL host must
	// stay inside the owning application's allowed webhook domains.
	OAuthApplicationID *string `json:"oauth_application_id,omitempty"`
	CreatedBy          *string `json:"created_by,omitempty"`

	// VerifiedAt is non-nil once the endpoint passed ownership verification.
	// Only verified endpoints receive the real event stream.
	VerifiedAt *time.Time `json:"verified_at,omitempty"`
	// OwnershipConfirmed is true once the receiver echoed the challenge back,
	// which proves control of the URL rather than mere reachability.
	OwnershipConfirmed bool `json:"ownership_confirmed"`

	// AutoDisabledAt is set when sustained failures tripped the breaker.
	AutoDisabledAt *time.Time `json:"auto_disabled_at,omitempty"`
	DisabledReason *string    `json:"disabled_reason,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Webhook is a webhook endpoint as returned by the API.

func (*Webhook) Verified

func (w *Webhook) Verified() bool

Verified reports whether the endpoint has passed ownership verification.

type WebhookChallengePayload added in v0.3.0

type WebhookChallengePayload struct {
	Challenge string `json:"challenge"`
	// EndpointID identifies which of your endpoints is being verified, for a
	// handler serving several.
	EndpointID string `json:"endpoint_id"`
	// Message is human-readable instructions, for someone reading the delivery
	// in a log.
	Message string `json:"message,omitempty"`
}

WebhookChallengePayload is the EventEndpointTest body, delivered only to the endpoint being verified. Echo WebhookChallengePayload.Challenge back — in your response body, or in the WebhookChallengeHeader — to confirm the endpoint and start receiving real events.

Take the token from here, after verifying the signature, rather than from the request header: the header is an unsigned convenience copy.

type WebhookCreateParams

type WebhookCreateParams struct {
	// URL is the HTTPS endpoint that will receive deliveries. Private and
	// loopback addresses are rejected.
	URL         string `json:"url"`
	Description string `json:"description,omitempty"`
	// EventTypes are the keys to subscribe to. An empty list subscribes to
	// every non-firehose event.
	EventTypes []string `json:"event_types,omitempty"`
	// Enabled defaults to true when nil.
	Enabled *bool `json:"enabled,omitempty"`
}

WebhookCreateParams registers a webhook endpoint.

type WebhookDelivery

type WebhookDelivery struct {
	ID             string `json:"id"`
	EndpointID     string `json:"endpoint_id"`
	OrganizationID string `json:"organization_id"`
	EventType      string `json:"event_type"`
	// EventID is stable across retries of the same event.
	EventID string `json:"event_id"`
	// Payload is the exact body POSTed to the endpoint.
	Payload json.RawMessage `json:"payload"`
	// Status is one of the Delivery* constants.
	Status        string     `json:"status"`
	AttemptCount  int        `json:"attempt_count"`
	MaxAttempts   int        `json:"max_attempts"`
	NextAttemptAt time.Time  `json:"next_attempt_at"`
	LastAttemptAt *time.Time `json:"last_attempt_at,omitempty"`

	ResponseStatus      *int    `json:"response_status,omitempty"`
	ResponseBodyExcerpt *string `json:"response_body_excerpt,omitempty"`
	ErrorReason         *string `json:"error_reason,omitempty"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

WebhookDelivery is one delivery record, updated in place as attempts progress.

type WebhookDeliveryListParams

type WebhookDeliveryListParams struct {
	ListOptions
	// EndpointID narrows to one endpoint. It is ignored by
	// [WebhookService.EndpointDeliveries], which takes the id in the path.
	EndpointID string
	// Status is one of the Delivery* constants.
	Status string
	// EventType is one of the Event* constants.
	EventType string
}

WebhookDeliveryListParams filters the delivery log.

type WebhookDrop

type WebhookDrop struct {
	EventType      string    `json:"event_type"`
	Day            time.Time `json:"day"`
	DroppedWindows int       `json:"dropped_windows"`
	LastDroppedAt  time.Time `json:"last_dropped_at"`
}

WebhookDrop is a daily rollup of events the dispatch throttle dropped, so rate limiting is visible rather than silent.

type WebhookEndpointList

type WebhookEndpointList struct {
	Endpoints  []Webhook                `json:"endpoints"`
	EventTypes []WebhookEventDescriptor `json:"event_types"`
}

WebhookEndpointList is the response from WebhookService.List: the organization's endpoints plus the full event catalog, so a picker can be rendered without a second call.

type WebhookEvent

type WebhookEvent struct {
	// ID is the event id, stable across retries. Deduplicate on it.
	ID string `json:"id"`
	// EventType is the event key; compare it against the Event* constants.
	EventType      string    `json:"event_type"`
	OrganizationID string    `json:"organization_id"`
	CreatedAt      time.Time `json:"created_at"`
	// Data is the event-specific payload, left raw.
	Data json.RawMessage `json:"data"`
}

WebhookEvent is the decoded body of a delivery, as produced by WebhookService.ConstructEvent. Switch on EventType, then unmarshal Data into the concrete payload for that event.

func ConstructWebhookEvent

func ConstructWebhookEvent(payload []byte, signatureHeader, secret string, tolerance time.Duration) (*WebhookEvent, error)

ConstructWebhookEvent is WebhookService.ConstructEvent with an explicit timestamp tolerance. A tolerance of zero or less disables the replay check.

func (*WebhookEvent) Into added in v0.3.0

func (e *WebhookEvent) Into(v any) error

Into decodes the event's WebhookEvent.Data into v, which should be a pointer to the payload type for WebhookEvent.EventType. Most events carry the ids of what changed rather than the resource itself: refetch it over the REST API when you need its current state.

type WebhookEventDescriptor

type WebhookEventDescriptor struct {
	// Type is the event key, for example [EventCampaignReplyReceived].
	Type string `json:"type"`
	// Category groups the event for a picker; one of the WebhookCat* values.
	Category    string `json:"category"`
	Description string `json:"description"`
	// Firehose marks high-volume events. They are opt-in only: an endpoint
	// with an empty event filter does not receive them.
	Firehose bool `json:"firehose"`
}

WebhookEventDescriptor is one entry in the public event catalog.

type WebhookEventName

type WebhookEventName = string

WebhookEventName is a stable webhook event key. The constants below are the full catalog at this SDK release; WebhookService.EventTypes is authoritative and stays current.

const (
	// Mailbox lifecycle and health.
	EventEmailAccountConnected    WebhookEventName = "email_account.connected"
	EventEmailAccountRemoved      WebhookEventName = "email_account.removed"
	EventEmailAccountDisconnected WebhookEventName = "email_account.disconnected"
	EventEmailAccountError        WebhookEventName = "email_account.error"
	// EventEmailAccountSynced is a firehose event: one per completed sync
	// pass, so subscribe to it explicitly or not at all.
	EventEmailAccountSynced        WebhookEventName = "email_account.synced"
	EventEmailAccountHealthChanged WebhookEventName = "email_account.health_changed"

	// Campaign send pipeline. Everything up to and including the click is a
	// firehose event: one per message, excluded from the "all events" wildcard
	// and delivered only to an endpoint that lists it explicitly.
	EventCampaignEmailSent      WebhookEventName = "campaign.email_sent"
	EventCampaignEmailDelivered WebhookEventName = "campaign.email_delivered"
	EventCampaignEmailOpened    WebhookEventName = "campaign.email_opened"
	EventCampaignEmailClicked   WebhookEventName = "campaign.email_clicked"
	EventCampaignEmailBounced   WebhookEventName = "campaign.email_bounced"
	EventCampaignReplyReceived  WebhookEventName = "campaign.reply_received"
	EventCampaignUnsubscribed   WebhookEventName = "campaign.unsubscribed"

	// Campaign lifecycle and configuration.
	EventCampaignStarted   WebhookEventName = "campaign.started"
	EventCampaignPaused    WebhookEventName = "campaign.paused"
	EventCampaignCompleted WebhookEventName = "campaign.completed"
	EventCampaignCreated   WebhookEventName = "campaign.created"
	EventCampaignUpdated   WebhookEventName = "campaign.updated"
	EventCampaignDeleted   WebhookEventName = "campaign.deleted"
	// EventCampaignDeliverabilityWarning fires when a campaign's rolling
	// bounce or complaint rate enters the early-warning band, short of an
	// auto-pause.
	EventCampaignDeliverabilityWarning WebhookEventName = "campaign.deliverability_warning"
	// EventCampaignAction fires from a notify action node in a sequence.
	EventCampaignAction WebhookEventName = "campaign.action"

	// Warmup. EventWarmupEmailSent is a firehose event: one per warmup
	// message, which is the highest-volume family the platform emits.
	EventWarmupEmailSent       WebhookEventName = "warmup.email_sent"
	EventWarmupHealthChanged   WebhookEventName = "warmup.health_changed"
	EventWarmupPlacementInSpam WebhookEventName = "warmup.placement_in_spam"
	EventWarmupQuarantined     WebhookEventName = "warmup.quarantined"
	EventWarmupBlocked         WebhookEventName = "warmup.blocked"

	// Deliverability.
	EventDeliverabilityBounce    WebhookEventName = "deliverability.bounce"
	EventDeliverabilityComplaint WebhookEventName = "deliverability.complaint"

	// Meetings booked through a connected scheduling provider.
	EventMeetingBooked      WebhookEventName = "meeting.booked"
	EventMeetingRescheduled WebhookEventName = "meeting.rescheduled"
	EventMeetingCanceled    WebhookEventName = "meeting.canceled"

	// Inbox mail. These are firehose events: subscribe explicitly.
	EventInboxEmailReceived WebhookEventName = "inbox.email_received"
	EventInboxEmailUpdated  WebhookEventName = "inbox.email_updated"
	EventInboxEmailDeleted  WebhookEventName = "inbox.email_deleted"
	EventInboxReplyReceived WebhookEventName = "inbox.reply_received"

	// Contacts.
	//
	// EventContactCreated carries a payload of its own
	// ([ContactCreatedPayload]) rather than the generic audit shape the other
	// two use, and it is deliberately quiet: see the payload's documentation
	// for when it does not fire.
	EventContactCreated WebhookEventName = "contact.created"
	EventContactUpdated WebhookEventName = "contact.updated"
	EventContactDeleted WebhookEventName = "contact.deleted"

	// EventFormSubmitted fires when a hosted form receives a submission. It is
	// categorized under Contact, not a form category of its own, because lead
	// capture is what it is for. Payload: [FormSubmittedPayload].
	EventFormSubmitted WebhookEventName = "form.submitted"

	// Bulk import and export.
	EventBulkOperationStarted   WebhookEventName = "bulk_operation.started"
	EventBulkOperationCompleted WebhookEventName = "bulk_operation.completed"
	EventBulkOperationFailed    WebhookEventName = "bulk_operation.failed"

	// Automations.
	EventAutomationCreated WebhookEventName = "automation.created"
	EventAutomationUpdated WebhookEventName = "automation.updated"
	EventAutomationDeleted WebhookEventName = "automation.deleted"
	EventAutomationRun     WebhookEventName = "automation.run"

	// Templates.
	EventTemplateCreated WebhookEventName = "template.created"
	EventTemplateUpdated WebhookEventName = "template.updated"
	EventTemplateDeleted WebhookEventName = "template.deleted"

	// Team and access governance.
	EventTeamMemberInvited WebhookEventName = "team.member_invited"
	EventTeamMemberRemoved WebhookEventName = "team.member_removed"
	EventRoleCreated       WebhookEventName = "role.created"
	EventRoleUpdated       WebhookEventName = "role.updated"
	EventRoleDeleted       WebhookEventName = "role.deleted"

	// CRM.
	EventCRMDealCreated    WebhookEventName = "crm.deal_created"
	EventCRMDealUpdated    WebhookEventName = "crm.deal_updated"
	EventCRMDealDeleted    WebhookEventName = "crm.deal_deleted"
	EventCRMTaskCreated    WebhookEventName = "crm.task_created"
	EventCRMTaskUpdated    WebhookEventName = "crm.task_updated"
	EventCRMNoteCreated    WebhookEventName = "crm.note_created"
	EventCRMPipelineUpdate WebhookEventName = "crm.pipeline_updated"

	// Workspace.
	EventLeadSyncSourceUpdated WebhookEventName = "lead_sync_source.updated"
	EventSettingsUpdated       WebhookEventName = "settings.updated"
	EventSubscriptionUpdated   WebhookEventName = "subscription.updated"

	// EventCustom is the developer-defined event published by a fire-event
	// sequence node.
	EventCustom WebhookEventName = "custom.event"
	// EventEndpointTest is the verification ping, delivered only to the
	// endpoint being verified.
	EventEndpointTest WebhookEventName = "webhook.test"
)

Webhook event keys.

type WebhookService

type WebhookService service

WebhookService manages webhook endpoints and their delivery log.

Each endpoint receives signed HTTP POSTs for the event types it subscribes to. Verify the WebhookSignatureHeader on every delivery with VerifyWebhookSignature — or, more conveniently, WebhookService.ConstructEvent — before trusting the payload.

A new endpoint does not receive the real event stream until it has proved ownership of its URL: call WebhookService.Verify, then echo the challenge value back from your handler.

func (*WebhookService) ConstructEvent

func (s *WebhookService) ConstructEvent(payload []byte, signatureHeader, secret string) (*WebhookEvent, error)

ConstructEvent verifies a delivery's signature and decodes its payload.

Pass the raw, unmodified request body and the value of the WebhookSignatureHeader. It returns ErrInvalidWebhookSignature when the signature does not match, or ErrWebhookSignatureExpired when the timestamp falls outside DefaultWebhookTolerance.

func handler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	event, err := client.Webhooks.ConstructEvent(body, r.Header.Get(warmbly.WebhookSignatureHeader), secret)
	if err != nil {
		http.Error(w, "bad signature", http.StatusBadRequest)
		return
	}
	switch event.EventType {
	case warmbly.EventCampaignReplyReceived:
		// ...
	}
}
Example

Verify every webhook delivery before trusting its payload.

package main

import (
	"fmt"
	"log"

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

func main() {
	client, _ := warmbly.New(warmbly.WithAPIKey("wmbly_..."))

	var body []byte            // the raw request body, unmodified
	var signatureHeader string // r.Header.Get(warmbly.WebhookSignatureHeader)
	const secret = "whsec_..."

	event, err := client.Webhooks.ConstructEvent(body, signatureHeader, secret)
	if err != nil {
		// Either the signature did not match or it was too old to trust.
		log.Fatal(err)
	}

	switch event.EventType {
	case warmbly.EventCampaignReplyReceived:
		fmt.Println("someone replied")
	case warmbly.EventWarmupBlocked:
		fmt.Println("a mailbox was blocked from the warmup pool")
	}
}

func (*WebhookService) Create

Create registers a new endpoint. The returned WebhookWithSecret carries the signing secret, which is only ever available here.

The endpoint receives no real events until it passes verification: call WebhookService.Verify and echo the challenge back.

func (*WebhookService) Delete

func (s *WebhookService) Delete(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Delete permanently removes an endpoint.

func (*WebhookService) Deliveries

Deliveries returns a page of the delivery log across every endpoint.

func (*WebhookService) Drops

func (s *WebhookService) Drops(ctx context.Context, opts ...RequestOption) ([]WebhookDrop, *Response, error)

Drops returns the daily rollup of events the dispatch throttle dropped.

func (*WebhookService) EndpointDeliveries

func (s *WebhookService) EndpointDeliveries(ctx context.Context, id string, params *WebhookDeliveryListParams, opts ...RequestOption) (*Page[WebhookDelivery], error)

EndpointDeliveries returns a page of one endpoint's delivery log.

func (*WebhookService) EventTypes

EventTypes returns the full catalog of deliverable events.

func (*WebhookService) List

List returns the organization's webhook endpoints alongside the full event catalog. The list is not paginated.

func (*WebhookService) Redeliver

func (s *WebhookService) Redeliver(ctx context.Context, deliveryID string, opts ...RequestOption) (*Response, error)

Redeliver re-queues a delivery for a fresh attempt cycle, keeping the same event id so a receiver deduplicating on it stays correct.

func (*WebhookService) RotateSecret

func (s *WebhookService) RotateSecret(ctx context.Context, id string, opts ...RequestOption) (string, *Response, error)

RotateSecret issues a new signing secret and returns it. The previous secret stops verifying immediately, so deploy the new one before rotating.

func (*WebhookService) Update

func (s *WebhookService) Update(ctx context.Context, id string, params *WebhookUpdateParams, opts ...RequestOption) (*Webhook, *Response, error)

Update replaces an endpoint's configuration.

func (*WebhookService) Verify

func (s *WebhookService) Verify(ctx context.Context, id string, opts ...RequestOption) (*Response, error)

Verify sends an ownership challenge to the endpoint. The challenge arrives as a signed EventEndpointTest delivery carrying a WebhookChallengePayload; echo the token from its verified payload back — in the WebhookChallengeHeader of your response, or in the body — to confirm the endpoint and start receiving events.

It returns as soon as the challenge is queued, not once it lands: the endpoint's OwnershipConfirmed flag is what says it worked.

type WebhookUpdateParams

type WebhookUpdateParams struct {
	URL         string   `json:"url"`
	Description string   `json:"description,omitempty"`
	EventTypes  []string `json:"event_types,omitempty"`
	Enabled     *bool    `json:"enabled,omitempty"`
}

WebhookUpdateParams replaces an endpoint's configuration. Unlike most update params these are not merged: the API replaces URL, description, event types and enabled state wholesale, so send the complete desired state.

The secret is not changed here; use WebhookService.RotateSecret. Changing the URL host re-arms ownership verification.

type WebhookWithSecret

type WebhookWithSecret struct {
	Webhook
	Secret string `json:"secret"`
}

WebhookWithSecret is a newly created endpoint together with its signing secret, which is returned only at creation. Store it securely; afterwards it can only be replaced with WebhookService.RotateSecret.

type WebsiteTrackingService added in v0.3.0

type WebsiteTrackingService service

WebsiteTrackingService configures website visitor tracking: the snippet a workspace installs on its own site so page views land on a contact's timeline. It governs consent mode, location precision, allowed hosts, retention and the site key.

These routes are session-only and need the manage-settings organization permission (OrgPermManageSettings): they are privacy governance, so they stay off the API-key surface. The snippet itself is not served by the API; it is loaded from the deployment's tracking host (WebsiteTrackingSettings.TrackingHost) as https://<tracking_host>/tracking.js with the site key in data-site, and page views go to that host too.

func (*WebsiteTrackingService) RotateKey added in v0.3.0

RotateKey issues a new site key and returns the settings carrying it. The old key stops working at once, so every installed snippet has to be updated. It takes no body and is safe to repeat; each call issues another key.

func (*WebsiteTrackingService) Settings added in v0.3.0

Settings returns the workspace's tracking configuration, creating a disabled one with a fresh site key on first call.

func (*WebsiteTrackingService) UpdateSettings added in v0.3.0

UpdateSettings changes the tracking configuration and returns the result.

type WebsiteTrackingSettings added in v0.3.0

type WebsiteTrackingSettings struct {
	OrganizationID string `json:"organization_id"`
	// Enabled gates ingestion entirely.
	Enabled bool `json:"enabled"`
	// SiteKey identifies the workspace in the snippet. It is public — it only
	// says which workspace a page view belongs to — but a leaked copy can be
	// retired with [WebsiteTrackingService.RotateKey].
	SiteKey string `json:"site_key"`
	// ConsentMode is [WebsiteConsentExplicit] or [WebsiteConsentImplicit].
	ConsentMode string `json:"consent_mode"`
	// LocationPrecision is one of the WebsiteLocation* constants.
	LocationPrecision string `json:"location_precision"`
	// AllowedHosts are the hosts the site runs on; a host covers its
	// subdomains. Page views from any other host are ignored, and campaign
	// links only identify a visitor when they lead to one of these. Empty
	// means views from anywhere are recorded but never tied to a contact.
	AllowedHosts []string `json:"allowed_hosts"`
	// RetentionDays is how long page views are kept, between
	// [WebsiteRetentionMinDays] and [WebsiteRetentionMaxDays].
	RetentionDays int       `json:"retention_days"`
	UpdatedAt     time.Time `json:"updated_at"`
	// TrackingHost is the deployment's tracking host, so a client can render
	// the exact snippet. Empty when the install has none, in which case
	// tracking cannot work.
	TrackingHost string `json:"tracking_host"`
}

WebsiteTrackingSettings is a workspace's tracking configuration. It is created on first read with tracking disabled, so every workspace has a site key to show but nothing is accepted until Enabled is set.

type WebsiteTrackingUpdateParams added in v0.3.0

type WebsiteTrackingUpdateParams struct {
	Enabled           *bool     `json:"enabled,omitempty"`
	ConsentMode       *string   `json:"consent_mode,omitempty"`
	LocationPrecision *string   `json:"location_precision,omitempty"`
	AllowedHosts      *[]string `json:"allowed_hosts,omitempty"`
	RetentionDays     *int      `json:"retention_days,omitempty"`
}

WebsiteTrackingUpdateParams changes the tracking configuration. Nil fields are left unchanged; an empty (non-nil) AllowedHosts clears the list.

type WriteParams

type WriteParams struct {
	Prompt string `json:"prompt"`
	// Tone steers the register, for example "direct" or "friendly".
	Tone string `json:"tone,omitempty"`
}

WriteParams asks for a fresh piece of copy.

Directories

Path Synopsis
examples
analytics command
Command analytics reads aggregate analytics: the workspace dashboard, deliverability health over the last 30 days, and per-mailbox status.
Command analytics reads aggregate analytics: the workspace dashboard, deliverability health over the last 30 days, and per-mailbox status.
apikeys command
Command apikeys demonstrates minting, listing and auditing API keys.
Command apikeys demonstrates minting, listing and auditing API keys.
campaigns command
Command campaigns demonstrates building and running a campaign: create it with its sequence, preflight it, send a test, start it, and list campaigns with automatic pagination.
Command campaigns demonstrates building and running a campaign: create it with its sequence, preflight it, send a test, start it, and list campaigns with automatic pagination.
contacts command
Command contacts demonstrates adding contacts, searching them with the faceted filter, reading the contact 360 view, and applying a bulk update.
Command contacts demonstrates adding contacts, searching them with the faceted filter, reading the contact 360 view, and applying a bulk update.
emails command
Command emails demonstrates managing connected mailboxes: listing them, driving warmup, checking whether a mailbox is blocked from the warmup pool, and sending a one-off message.
Command emails demonstrates managing connected mailboxes: listing them, driving warmup, checking whether a mailbox is blocked from the warmup pool, and sending a one-off message.
errors command
Command errors demonstrates error handling, retry configuration, and reading rate-limit state from a response.
Command errors demonstrates error handling, retry configuration, and reading rate-limit state from a response.
gateway command
Command gateway streams realtime events from a workspace and logs them until interrupted.
Command gateway streams realtime events from a workspace and logs them until interrupted.
oauth command
Command oauth demonstrates the OAuth 2.1 authorization-code flow with PKCE.
Command oauth demonstrates the OAuth 2.1 authorization-code flow with PKCE.
oauthapps command
Command oauthapps demonstrates registering and managing an OAuth 2.1 application, then using its credentials for machine-to-machine access through the client-credentials grant.
Command oauthapps demonstrates registering and managing an OAuth 2.1 application, then using its credentials for machine-to-machine access through the client-credentials grant.
organization command
Command organization demonstrates managing the workspace and its people: read its settings and limits, list members and roles, invite a teammate, then remove them.
Command organization demonstrates managing the workspace and its people: read its settings and limits, list members and roles, invite a teammate, then remove them.
templates command
Command templates demonstrates the lifecycle of a reply template: create, score the copy for spam risk, update, render, reorder and delete.
Command templates demonstrates the lifecycle of a reply template: create, score the copy for spam risk, update, render, reorder and delete.
webhooks command
Command webhooks demonstrates both sides of webhooks: registering an endpoint with the API, and running an HTTP receiver that verifies the signature on every delivery before acting on it.
Command webhooks demonstrates both sides of webhooks: registering an endpoint with the API, and running an HTTP receiver that verifies the signature on every delivery before acting on it.
Package gateway is a client for the Warmbly realtime gateway: a persistent websocket that streams workspace events — campaign progress, engagement pulses, inbox arrivals, warmup health, automation runs — as they happen, instead of polling the REST API.
Package gateway is a client for the Warmbly realtime gateway: a persistent websocket that streams workspace events — campaign progress, engagement pulses, inbox arrivals, warmup health, automation runs — as they happen, instead of polling the REST API.
internal
wsconn
Package wsconn is a minimal, dependency-free RFC 6455 WebSocket client.
Package wsconn is a minimal, dependency-free RFC 6455 WebSocket client.

Jump to

Keyboard shortcuts

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