Documentation
¶
Overview ¶
Package httpapi is the e2a v1 HTTP contract layer, built on Huma + chi.
It exists to make the OpenAPI 3.1 spec the single source of truth: every operation is declared with typed Go input/output structs, and Huma emits the spec *and* validates requests from those same definitions, so the handler is the contract and the spec cannot drift by construction (api-v1-redesign §6). This package is the foundation slice (Slice 1): the canonical error envelope, cursor pagination, idempotency, and shared middleware that every ported operation reuses.
chi owns the `/v1` prefix and falls back to the legacy gorilla/mux for the remaining non-v1 routes (OAuth, session auth, health/feedback, the magic-link approve/reject pages). The `/api/v1` surface this strangler replaced is fully retired — no `/api/v1` route is registered anymore.
Index ¶
- Constants
- Variables
- func DecodeCursor(secrets []string, cursor string, dst any) error
- func EncodeCursor(secret string, payload any) (string, error)
- func RequestFromContext(ctx context.Context) *http.Request
- func RequestIDFromContext(ctx context.Context) string
- func WriteError(w http.ResponseWriter, r *http.Request, status int, code, message string)
- type APIKeyView
- type AccountUserView
- type AccountView
- type AddressParam
- type AgentCreateEnforcer
- type AgentCreator
- type AgentDeleter
- type AgentGetter
- type AgentLister
- type AgentSuppressionView
- type AgentTrashOp
- type AgentView
- type AttachmentMetaView
- type AttachmentStore
- type AttachmentView
- type Authenticator
- type ConversationDetailView
- type ConversationGetter
- type ConversationIDParam
- type ConversationLister
- type ConversationSummaryView
- type CoveringDomainLookup
- type CreateAPIKeyRequest
- type CreateAPIKeyResponse
- type CreateAgentRequest
- type CreateAgentSuppressionRequest
- type CreateTemplateRequest
- type CreateWebhookRequest
- type CreateWebhookResponse
- type DNSRecord
- type DeleteAgentResult
- type DeleteApiKeyResult
- type DeleteConfirm
- type DeleteDomainResult
- type DeleteMessageResult
- type DeleteSuppressionResult
- type DeleteTemplateResult
- type DeleteWebhookResult
- type DeploymentInfoView
- type Deps
- type DomainCheckResult
- type DomainLookup
- type DomainParam
- type DomainView
- type ErrorBody
- type ErrorEnvelope
- type EventEnvelope
- type EventQuery
- type FieldError
- type ForwardRequest
- type HoldReasonView
- type IdemStore
- type LimitExceededDetails
- type LimitExceededEnvelope
- type LimitExceededErrorBody
- type LimitsCapsView
- type LimitsUsageView
- type ListConversationsInput
- type ListDeliveriesInput
- type ListEventsInput
- type ListMessagesInput
- type MXResolver
- type MessageBodyView
- type MessageGetter
- type MessageIDParam
- type MessageLister
- type MessageParsedView
- type MessageSummaryView
- type MessageTrashOp
- type MessageView
- type Page
- type PageParams
- type PayloadTooLargeDetails
- type PrincipalAuthenticator
- type ProtectionConfigRequest
- type ProtectionConfigView
- type ProtectionDirectionRequest
- type ProtectionDirectionView
- type ProtectionFindingView
- type ProtectionGateRequest
- type ProtectionGateView
- type ProtectionHoldsRequest
- type ProtectionHoldsView
- type ProtectionScanRequest
- type ProtectionScanView
- type RateLimitedDetails
- type RateLimitedEnvelope
- type RateLimitedErrorBody
- type RateSnapshot
- type RedeliverDelivery
- type RedeliverEventInput
- type RedeliverEventRequest
- type RedeliverView
- type RegisterDomainRequest
- type RejectRequest
- type RejectResultView
- type RenderedTemplateView
- type ReplyRequest
- type RetryAfterDetails
- type ReviewView
- type SendEmailRequest
- type SendResultView
- type SendingRampView
- type Server
- type StarterTemplateAliasParam
- type StarterTemplateDetailView
- type StarterTemplateVariableView
- type StarterTemplateView
- type SuppressionView
- type TemplateData
- type TemplateIDParam
- type TemplatePartError
- type TemplateSummaryView
- type TemplateView
- type TestWebhookRequest
- type TestWebhookResponse
- type ThreatCategoryView
- type TooManyRecipientsDetails
- type UnsubscribeOptions
- type UpdateAgentRequest
- type UpdateMessageRequest
- type UpdateMessageResultView
- type UpdateTemplateRequest
- type UpdateWebhookRequest
- type ValidateTemplateRequest
- type ValidateTemplateResponse
- type ValidationErrorDetails
- type VerifyDomainView
- type WebhookDeliveryView
- type WebhookFiltersRequest
- type WebhookFiltersView
- type WebhookIDParam
- type WebhookView
Constants ¶
const APIVersion = "1.0.0"
APIVersion is the public /v1 contract version. Single source for both the OpenAPI document version (huma.DefaultConfig) and the GET /v1/info `version` field; tracks the repo-root VERSION file.
Variables ¶
var ErrInvalidCursor = errors.New("invalid pagination cursor")
ErrInvalidCursor is returned when a cursor fails to decode. Handlers map it to a 400 with code "invalid_cursor".
Functions ¶
func DecodeCursor ¶
DecodeCursor verifies a cursor's HMAC and reverses it into dst. A malformed, tampered, or wrong-secret cursor yields ErrInvalidCursor rather than a generic error so callers branch on a stable sentinel. An empty cursor is treated as "start from the beginning" — dst is left untouched and nil is returned.
secrets is tried in order and the signature is compared in constant time (hmac.Equal, mirroring approvaltoken.Verify). Accepting a slice supports HMAC-secret rotation: a cursor signed under an old secret keeps verifying until that secret is retired. Today callers pass a single-element slice.
Old unsigned cursors (plain base64url(json) with no "." signature segment, as emitted before issue #144 M2) no longer verify and are hard-rejected with ErrInvalidCursor. Cursors are ephemeral, so a client mid-pagination simply restarts the query.
func EncodeCursor ¶
EncodeCursor serializes an arbitrary cursor payload (the position + filter snapshot a resource needs to resume) into the opaque, URL-safe, tamper-evident string clients echo back.
The cursor is HMAC-signed (issue #144, finding M2): a client can no longer decode the base64, edit a field, and re-encode it — any such edit breaks the signature and DecodeCursor rejects it. The cursor remains opaque to clients; the payload shape stays private to each resource.
Format:
base64url(json_payload) + "." + base64url(hmac_sha256(secret, base64url(json_payload)))
The MAC is computed over the LITERAL emitted base64url(json) segment (not a re-marshaled struct) so encode and verify are byte-canonical and cannot drift. secret is the deployment HMAC secret (config.Signing.HMACSecret) — the same deployment key used by approval tokens, so there is no new key to manage.
func RequestFromContext ¶
RequestFromContext recovers the raw request stashed by withRawRequest.
func RequestIDFromContext ¶
RequestIDFromContext returns the request id stamped by the requestID middleware, or "" if none is present.
func WriteError ¶
WriteError writes the canonical v1 error envelope to a raw ResponseWriter for handlers OUTSIDE this package that bypass Huma — specifically the WebSocket upgrade handshake (internal/ws), which authenticates and authorizes BEFORE the upgrade and so rejects a bad handshake with a normal HTTP response. Routing those rejections through here makes the WS handshake body byte-for-byte consistent with every /v1 REST endpoint: {error:{code,message,request_id}} + X-Request-Id. The caller supplies the status and a code from the REST vocabulary (unauthorized / forbidden / not_found / invalid_request); status codes are the caller's to choose so this never rewrites them.
Types ¶
type APIKeyView ¶
type APIKeyView struct {
ID string `json:"id"`
Name string `json:"name"`
KeyPrefix string `json:"key_prefix" doc:"Non-secret visible prefix (e.g. e2a_acct_… / e2a_agt_…)."`
// Scope is an OPEN set on this response view (evolving vocabulary); the
// REQUEST-side CreateAPIKeyRequest.scope keeps its closed enum, which is
// where validation belongs.
Scope string `` /* 203-byte string literal not displayed */
Agent string `json:"agent_email,omitempty" doc:"Bound inbox email for agent-scoped keys; omitted for account scope."`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
APIKeyView is the non-secret metadata for an API key (list + create). The plaintext secret is never in this shape — it appears once, in CreateAPIKeyResponse.Key.
type AccountUserView ¶
AccountUserView is the authenticated principal's identity (A-1). Returned by GET /v1/account (MCP `whoami`) so an agent/operator can answer "who am I" without a follow-up call.
type AccountView ¶
type AccountView struct {
User AccountUserView `json:"user"`
// Scope is an OPEN set on this response view (evolving vocabulary), not a
// closed enum — see docs/api.md "Versioning & stability".
Scope string `` /* 166-byte string literal not displayed */
AgentAddress string `json:"agent_email,omitempty"`
PlanCode string `json:"plan_code"`
Limits LimitsCapsView `json:"limits"`
Usage LimitsUsageView `json:"usage"`
UpgradeURL string `json:"upgrade_url"`
}
AccountView is the authenticated account: identity + plan + limits + usage (A-2, formerly LimitsView). `whoami` maps here. It is scope-aware: `scope` is always set, and `agent_email` is populated ONLY for agent-scoped credentials (where the credential *is* a single agent) — omitted for account scope, which spans many agents.
type AddressParam ¶
type AddressParam struct {
Address string `path:"email" doc:"The agent's full email address, e.g. support@acme.com."`
}
AddressParam is the shared path input for per-agent operations. The address is the agent's full email and the resource identifier (api-v1-redesign decision 1); Huma URL-decodes it from the path.
type AgentCreateEnforcer ¶
AgentCreateEnforcer mirrors enforcer.CheckAgentCreate; returns a limits.LimitExceededError when the per-user cap is hit.
type AgentCreator ¶
type AgentCreator func(ctx context.Context, email, domain, name, webhookURL, agentMode, userID string) (*identity.AgentIdentity, error)
AgentCreator mirrors store.CreateAgent. The webhookURL/agentMode params are retained for signature compatibility with the store but are ignored — the legacy columns were dropped (migration 029). Handlers pass "".
type AgentDeleter ¶
type AgentDeleter func(ctx context.Context, agentID, userID string) (messagesDeleted int64, err error)
AgentDeleter deletes an agent, returning the number of message rows removed by the cascade (surfaced in the DeleteAgentResult receipt).
type AgentGetter ¶
AgentGetter loads a single agent by its full email address (the identifier). Ownership is checked by the caller against the resolved agent's UserID.
type AgentLister ¶
type AgentLister func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterID string) ([]identity.AgentIdentity, error)
AgentLister returns one page of the agents owned by a user, keyset-paginated on (created_at, id): limit is the page size (the handler passes limit+1 to detect a further page; limit<=0 returns every agent, which the webhook filter-ownership validation relies on), and afterCreatedAt/afterID is the position from the previous page's last row (zero afterCreatedAt = first page). Injected as a narrow function so the foundation slice doesn't depend on the whole store.
type AgentSuppressionView ¶
type AgentSuppressionView struct {
AgentEmail string `json:"agent_email"`
Address string `json:"address"`
Reason string `json:"reason,omitempty"`
Source string `json:"source" doc:"How the block was created. Known values: unsubscribe, manual."`
CreatedAt time.Time `json:"created_at"`
}
AgentSuppressionView is one recipient block scoped to an exact sending agent. AgentEmail keeps list items self-describing outside their route.
type AgentTrashOp ¶
AgentTrashOp moves an agent into or out of trash without deleting messages.
type AgentView ¶
type AgentView struct {
Domain string `json:"domain" doc:"The exact domain in the agent email address."`
RegisteredDomain string `` /* 170-byte string literal not displayed */
Email string `json:"email"`
Name string `json:"name"`
DomainVerified bool `json:"domain_verified"`
CreatedAt time.Time `json:"created_at"`
// DeletedAt marks an agent in the trash (soft-deleted): set when the row
// was moved there, omitted on live agents. Trashed agents appear only via
// GET /v1/agents?deleted=true and restore/permanent-delete operations; they
// are purged ~30 days after deletion (docs/design/trash-soft-delete.md).
DeletedAt *time.Time `` /* 278-byte string literal not displayed */
}
AgentView is the public representation of an agent. The legacy webhook_url and agent_mode fields were dropped (migration 029). The per-agent screening/HITL config moved to the account-scoped /v1/agents/{email}/protection sub-resource (design 2026-06-22), so AgentView is identity + status only — an agent-scoped credential reading its own agent no longer learns its detection tuning (closes audit #13). The redundant `id` field was dropped: an agent's id IS its email, so `email` is the sole identity key (mirroring DomainView keyed on `domain` and Suppression on `address`).
type AttachmentMetaView ¶
type AttachmentMetaView = eventpayload.AttachmentMetaView
AttachmentMetaView is metadata for one attachment of a message — never the bytes. `index` is the stable 0-based attachment index (document order) used to fetch the bytes via the attachment endpoint. SizeBytes is the DECODED payload size (Content-Transfer-Encoding undone) — the size of the file a download yields, NOT the encoded size inside the raw MIME and NOT the message-level size_bytes (raw MIME length of the whole message).
Alias of the canonical eventpayload.AttachmentMetaView so the REST message views, the stable event payloads, and the account export publish ONE public attachment-metadata component schema.
type AttachmentStore ¶
type AttachmentStore interface {
// DownloadURL returns a short-lived URL the caller (or a sandboxed tool) can
// GET for the bytes, plus its expiry.
DownloadURL(agentEmail, messageID string, index int, ttl time.Duration) (downloadURL string, expiresAt time.Time, err error)
// VerifyDownload reports whether `token` authorizes downloading attachment
// `index` of `messageID` (and is unexpired). Identity is NOT in the token;
// the download handler additionally binds the message to the path agent.
VerifyDownload(token, messageID string, index int) bool
}
AttachmentStore mints (and verifies) a short-lived download for one attachment. The native adapter (below) returns a capability-token URL to e2a's own streaming route; a future object-storage adapter could return a presigned URL instead — the metadata/contract shape ({download_url, expires_at}) is the same.
func NewNativeAttachmentStore ¶
func NewNativeAttachmentStore(secret, publicURL string) AttachmentStore
NewNativeAttachmentStore returns the default (zero-dependency) attachment store.
type AttachmentView ¶
type AttachmentView struct {
Index int `json:"index"`
Filename string `json:"filename,omitempty"`
ContentType string `json:"content_type,omitempty"`
ContentID string `` /* 182-byte string literal not displayed */
SizeBytes int `` /* 229-byte string literal not displayed */
DownloadURL string `json:"download_url"`
ExpiresAt time.Time `json:"expires_at" format:"date-time"`
// Data is the base64 bytes, present ONLY when inline=true and the attachment
// is within the inline size cap.
Data string `json:"data,omitempty"`
}
AttachmentView is the metadata + short-lived download for one attachment. Bytes are NOT included unless inline was requested for a small file. SizeBytes is the DECODED payload size (Content-Transfer-Encoding undone) — exactly the byte count download_url serves and the count the inline cap is checked against. NOT the encoded size inside the raw MIME, and NOT the message-level size_bytes (raw MIME length of the whole message).
type Authenticator ¶
Authenticator resolves the calling user from the raw request. It is injected so this package reuses the *single* auth path that already lives in the agent layer (API key, OAuth bearer, session cookie) instead of forking a second one — there is exactly one place credentials are checked.
type ConversationDetailView ¶
type ConversationDetailView struct {
ConversationSummaryView
Participants []string `json:"participants" nullable:"false"`
Labels []string `json:"labels" nullable:"false"`
Messages []MessageSummaryView `json:"messages" nullable:"false"`
}
ConversationDetailView is the single-conversation shape: the summary fields (flattened via embedding, matching the legacy top-level layout) plus participants, labels, and the member message summaries.
type ConversationGetter ¶
type ConversationGetter func(ctx context.Context, agentID, conversationID string) (*identity.ConversationDetail, error)
ConversationGetter mirrors store.GetConversationByID(agentID, conversationID).
type ConversationIDParam ¶
ConversationIDParam is the path input for the single-conversation read.
type ConversationLister ¶
type ConversationLister func(ctx context.Context, filter identity.ConversationListFilter) ([]identity.ConversationSummary, error)
ConversationLister mirrors store.ListConversationsByAgent(filter).
type ConversationSummaryView ¶
type ConversationSummaryView struct {
ID string `json:"id"`
LastMessageAt time.Time `json:"last_message_at" format:"date-time"`
FirstMessageAt time.Time `json:"first_message_at" format:"date-time"`
MessageCount int `json:"message_count"`
InboundCount int `json:"inbound_count"`
OutboundCount int `json:"outbound_count"`
HasUnread bool `json:"has_unread"`
LatestSubject string `json:"latest_subject"`
LatestSender string `json:"latest_from"`
}
ConversationSummaryView is one conversation in the list. Timestamps are typed time.Time with format date-time (C-1) — consistent with every other timestamp in the surface (was previously plain strings, which generated an untyped `string` in the SDKs and risked a .getTime()/parse crash).
type CoveringDomainLookup ¶
CoveringDomainLookup mirrors store.LookupCoveringDomain(sub, userID): the create-time fallback that finds the most-specific registered parent domain the user owns which covers an agent's subdomain (label-boundary match). The caller checks its Verified state so a pending child cannot be masked. Nil is tolerated (feature disabled ⇒ exact-match-only behavior).
type CreateAPIKeyRequest ¶
type CreateAPIKeyRequest struct {
Name string `json:"name,omitempty" maxLength:"200" doc:"Human label for the key. At most 200 characters (Unicode code points)."`
ExpiresAt string `` /* 139-byte string literal not displayed */
Scope string `json:"scope,omitempty" enum:"account,agent" doc:"account = workspace admin (default); agent = bound to a single inbox."`
Agent string `json:"agent_email,omitempty" doc:"Inbox email to bind the key to; required when scope=agent."`
}
CreateAPIKeyRequest mirrors the legacy /api/keys body. scope defaults to account; scope=agent requires `agent_email` (an owned inbox email).
type CreateAPIKeyResponse ¶
type CreateAPIKeyResponse struct {
APIKeyView
Key string `json:"key" doc:"The secret key. Shown once; store it now — it cannot be retrieved later."`
}
CreateAPIKeyResponse is APIKeyView plus the one-time plaintext key — shown once at creation and never retrievable again.
type CreateAgentRequest ¶
type CreateAgentRequest struct {
Email string `` /* 129-byte string literal not displayed */
Name string `` /* 197-byte string literal not displayed */
}
CreateAgentRequest is the /v1 agent-create body. The legacy agent_mode and webhook_url fields were dropped (migration 029): push is delivered solely via the /v1/webhooks subscriber resource and WebSocket is open to all agents, so per-agent mode/webhook no longer exist. Fields are schema-optional (omitempty) so validation is handler-owned and uniform — the legacy 400 business-rule messages, not Huma's 422 (email itself can't be schema-required since the slug path derives it). CreateAgentRequest is the create-agent body (AG-1/AG-2). `email` is required and is the single create path: a custom-domain agent uses an email on a verified domain the caller owns; a shared-domain agent is just an email on the deployment's shared domain (e.g. xyz@agents.e2a.dev) — detected by the domain, not a separate `slug` field. The legacy `slug` field is dropped.
type CreateTemplateRequest ¶
type CreateTemplateRequest struct {
Name string `json:"name,omitempty" doc:"Human-readable template name. Required unless from_starter supplies the default."`
Alias string `` /* 126-byte string literal not displayed */
Subject string `` /* 139-byte string literal not displayed */
Body string `json:"text,omitempty" doc:"Plain-text body template source (no HTML escaping). Required unless from_starter is set."`
HTMLBody string `json:"html,omitempty" doc:"Optional HTML body template source ({{x}} is HTML-escaped, {{{x}}} is raw)."`
FromStarter string `` /* 392-byte string literal not displayed */
}
CreateTemplateRequest — either literal source (name, subject and text required; alias and html optional) or from_starter (a starter alias copied verbatim; name/alias default to the starter's). All template parts must parse. The required-ness of name/subject/text is enforced in the handler (validateTemplateFields), not the schema, because from_starter supplies them.
type CreateWebhookRequest ¶
type CreateWebhookRequest struct {
URL string `json:"url" doc:"Required, non-empty webhook delivery URL."`
Events []string `` /* 492-byte string literal not displayed */
Filters *WebhookFiltersRequest `json:"filters,omitempty"`
Description string `json:"description,omitempty"`
}
CreateWebhookRequest — url + events are required (WH-1). events items are constrained to the canonical vocabulary (WH-2; keep in sync with webhookpub.AllEventTypes).
type CreateWebhookResponse ¶
type CreateWebhookResponse struct {
WebhookView
SigningSecret string `json:"signing_secret"`
}
CreateWebhookResponse is WebhookView plus the one-time signing secret (WH-3), returned only by createWebhook. Persist the secret on receipt — it is never shown again (use rotate-secret to mint a new one).
type DNSRecord ¶
type DNSRecord struct {
Type string `json:"type" doc:"DNS record type. MX or TXT."`
Name string `json:"name" doc:"Record name (host). The apex domain for ownership/inbound_mx; an FQDN for dkim/mail_from records."`
Value string `json:"value" doc:"Record value. For MX records this is the mail-server host only; the priority is in the priority field."`
Priority *int `json:"priority" doc:"MX priority. Null for non-MX records."`
Purpose string `` /* 402-byte string literal not displayed */
Status string `` /* 1431-byte string literal not displayed */
}
DNSRecord is one row in the unified DomainView.DNSRecords array. It supersedes the legacy split of `dns_records` (an mx/txt/dkim object) + `sending_dns_records` (an array): every record the customer must publish — inbound and sending — now carries its own `purpose` and `status`, mirroring how peers (Resend/SendGrid) model DNS setup. MX records carry their priority in the dedicated `priority` field (TXT records leave it null) rather than embedding it in `value`.
type DeleteAgentResult ¶
type DeleteAgentResult struct {
Deleted bool `` /* 126-byte string literal not displayed */
Email string `json:"email" doc:"Email address of the deleted agent."`
MessagesDeleted int64 `json:"messages_deleted" doc:"Number of messages permanently removed by the cascade; zero when the agent is moved to trash."`
} // @name DeleteAgentResult
DeleteAgentResult confirms an agent delete. For a soft delete the agent is inactive in trash and messages_deleted is zero. For a permanent delete it is the number of message rows removed with the agent (webhook-delivery rows cascade from those and are not counted separately).
type DeleteApiKeyResult ¶
type DeleteApiKeyResult struct {
Deleted bool `json:"deleted" doc:"Always true — the key is revoked. A failed delete is an error envelope, never deleted:false."`
ID string `json:"id" doc:"ID of the revoked API key."`
} // @name DeleteApiKeyResult
DeleteApiKeyResult confirms an API-key revocation.
type DeleteConfirm ¶
type DeleteConfirm struct {
Confirm string `query:"confirm" enum:"DELETE" required:"true" doc:"Must be the literal DELETE — this action is irreversible."`
}
DeleteConfirm is the uniform destructive-delete guard embedded in every DELETE input across the /v1 surface (agents, domains, account, account suppressions, api-keys, templates, webhooks).
Modeling it as a required query param with a single-value enum makes Huma validate it declaratively: a request missing `confirm` — or carrying any value other than the literal "DELETE" — is rejected with 422 by the framework before the handler runs, and the constraint surfaces in the generated OpenAPI (required + enum: [DELETE]) so SDK/CLI codegen and the API reference show it. The guard exists to stop accidental raw-HTTP deletes; the hand-written SDK/CLI `delete(...)` layers auto-send `confirm=DELETE`, so explicit `.delete()` calls are never burdened with it.
This replaces the previous per-endpoint optional-string `confirm` that was checked in the handler with a bespoke error — that enforcement is now owned by Huma (422 invalid_request) so the gate is declared once and is identical on every delete op. The one op where confirm is only conditionally required (permanent message deletion — the default trash delete takes no confirm, so the param can't be schema-required) enforces the same contract in its handler and emits the identical 422 invalid_request.
type DeleteDomainResult ¶
type DeleteDomainResult struct {
Deleted bool `json:"deleted" doc:"Always true — the domain no longer exists. A failed delete is an error envelope, never deleted:false."`
Domain string `json:"domain" doc:"The deleted domain."`
} // @name DeleteDomainResult
DeleteDomainResult confirms a domain delete.
type DeleteMessageResult ¶
type DeleteMessageResult struct {
Deleted bool `` /* 146-byte string literal not displayed */
ID string `json:"id" doc:"ID of the deleted message."`
} // @name DeleteMessageResult
DeleteMessageResult confirms a message delete.
type DeleteSuppressionResult ¶
type DeleteSuppressionResult struct {
Deleted bool `` /* 132-byte string literal not displayed */
Address string `json:"address" doc:"The un-suppressed recipient address."`
} // @name DeleteSuppressionResult
DeleteSuppressionResult confirms a suppression-list removal.
type DeleteTemplateResult ¶
type DeleteTemplateResult struct {
Deleted bool `` /* 126-byte string literal not displayed */
ID string `json:"id" doc:"ID of the deleted template."`
} // @name DeleteTemplateResult
DeleteTemplateResult confirms a template delete.
type DeleteWebhookResult ¶
type DeleteWebhookResult struct {
Deleted bool `json:"deleted" doc:"Always true — the webhook no longer exists. A failed delete is an error envelope, never deleted:false."`
ID string `json:"id" doc:"ID of the deleted webhook."`
} // @name DeleteWebhookResult
DeleteWebhookResult confirms a webhook delete.
type DeploymentInfoView ¶
type DeploymentInfoView struct {
Version string `json:"version"`
SlugRegistrationEnabled bool `json:"slug_registration_enabled"`
PublicURL string `json:"public_url,omitempty"`
}
DeploymentInfoView is the public, unauthenticated deployment-discovery body. `version` (I-1) lets clients detect the API contract version pre-auth — the cheapest forward-compatibility lever.
type Deps ¶
type Deps struct {
Authenticator Authenticator
PrincipalAuthenticator PrincipalAuthenticator
// AuthChallenge builds the RFC 6750 §3 WWW-Authenticate header value for a
// request that failed authentication. Injected so the v1 surface advertises
// the Bearer scheme (and OAuth error params) on every 401 exactly like the
// legacy mux did, from the same definition (agent.API.WWWAuthenticateChallenge).
// Optional — nil disables the challenge header.
AuthChallenge func(r *http.Request) string
ListAgents AgentLister
GetAgent AgentGetter
GetMessage MessageGetter
ListMessages MessageLister
// ModifyMessageLabels applies a labels delta to a message scoped to an
// agent, returning the post-update set. Mirrors store.ModifyMessageLabels.
ModifyMessageLabels func(ctx context.Context, messageID, agentID string, add, remove []string) ([]string, error)
ListConversations ConversationLister
GetConversation ConversationGetter
CreateAgent AgentCreator
LookupDomain DomainLookup
LookupCoveringDomain CoveringDomainLookup
// ResolveMX backs the required create-time subdomain MX gate.
ResolveMX MXResolver
EnforceAgentCreate AgentCreateEnforcer
// UpdateAgentName updates an agent's display name (the only mutable field on
// the agent PATCH after the screening config moved to /protection).
UpdateAgentName func(ctx context.Context, agentID, userID, name string) error
// UpdateAgentProtection writes the full per-agent protection posture (gate +
// scan sensitivity + holds) for the /v1/agents/{email}/protection resource.
// Returns a validation error for an invalid posture, which the handler maps
// to 400 invalid_request.
UpdateAgentProtection func(ctx context.Context, agentID, userID string, cfg identity.ProtectionConfig) error
// DeleteAgent is the DEFAULT delete: soft (move to trash, restorable for
// identity.TrashRetention, docs/design/trash-soft-delete.md).
// PermanentDeleteAgent is the irreversible hard delete behind
// ?permanent=true; RestoreAgent brings a trashed agent back.
DeleteAgent AgentTrashOp
PermanentDeleteAgent AgentDeleter
RestoreAgent AgentTrashOp
// GetAgentAnyState loads an agent regardless of trash state (DeletedAt set
// when trashed) — the resolution path for restore / permanent delete, which
// must find agents the live GetAgent treats as nonexistent.
GetAgentAnyState AgentGetter
// ListDeletedAgents is the account's agent trash (GET /v1/agents?deleted=true).
ListDeletedAgents AgentLister
// Message trash ops (DELETE / POST restore on
// /v1/agents/{email}/messages/{id}): soft delete, restore, and the
// trash-only permanent purge.
DeleteMessage MessageTrashOp
RestoreMessage MessageTrashOp
PurgeMessage MessageTrashOp
// domains. ListDomains is keyset-paginated on (created_at, domain): the
// handler passes limit+1 to detect a further page (limit<=0 = all), and the
// after-key from the previous page's last row (zero afterCreatedAt = first
// page).
ListDomains func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterDomain string) ([]identity.Domain, error)
SendingRampSnapshot func(ctx context.Context, userID, domain string, now time.Time) (sendramp.Snapshot, error)
ClaimDomain func(ctx context.Context, domain, userID string) (*identity.Domain, error)
EnforceDomainCreate func(ctx context.Context, userID string) error
DeleteDomain func(ctx context.Context, domain, userID string) error
HasAgentsOnDomain func(ctx context.Context, domain, userID string) (bool, error)
// SMTPDomain is the relay's MX host, surfaced in the DNS records a
// domain must publish (config smtp.domain).
SMTPDomain string
// SESRegion is the AWS region of the SES sending identity
// (config sender_identity.ses_region). Non-empty ⇒ the sending feature is
// enabled: domainView emits the deterministic mail_from_* records. Empty ⇒
// sending is off and those records are omitted.
SESRegion string
// CursorSecret is the deployment HMAC secret (config.Signing.HMACSecret)
// used to sign/verify pagination cursors so they are tamper-evident
// (issue #144 M2). The same deployment key used by approvaltoken — no new
// key. Handlers pass it to EncodeCursor and wrap it
// in a 1-element slice for DecodeCursor (whose verify loop supports N for
// a future secret rotation). Empty in minimal test setups, which is fine:
// encode and verify stay consistent under the same (empty) key.
CursorSecret string
// Idempotency is the retry-safety store for unsafe writes (send/reply/
// forward/redeliver). Optional — nil disables the Idempotency-Key path.
Idempotency IdemStore
// outbound (the shared live delivery path extracted from agent.API)
DeliverOutbound func(ctx context.Context, user *identity.User, ag *identity.AgentIdentity, req outbound.SendRequest, msgType, replyToEmailMessageID string, referenced *identity.Message, idemCompleteTx agent.AcceptIdemCompleter) (*agent.OutboundResult, *agent.OutboundError)
SendTest func(ctx context.Context, ag *identity.AgentIdentity) (*agent.OutboundResult, *agent.OutboundError)
// PollSendOutcome reads an async send's current delivery_status for wait=sent.
// Optional — nil disables the wait valve (accepted is returned immediately).
PollSendOutcome func(ctx context.Context, messageID string) (identity.SendOutcome, error)
// HITL approve/reject (the held-draft decision)
ApprovePending func(ctx context.Context, userID, messageID, expectedAgentEmail string, ovr agent.ApproveOverrides, idemCompleteTx agent.ApproveIdemCompleter) (*identity.Message, *agent.OutboundError)
RejectPending func(ctx context.Context, userID, messageID, expectedAgentEmail, reason string) (*identity.Message, *agent.OutboundError)
EnforceMessageSend func(ctx context.Context, userID string) error
// Inbound review release — the held-screening decision (design 2026-06-22 §5).
// GetReviewMessage resolves a held message's direction so /approve+/reject can
// branch (it intentionally sees held inbound statuses, scoped to the resolved
// owned agent — account-scope only). ApproveInboundReview releases the message
// to the agent's inbox; RejectInboundReview drops it. Both fire the unified
// review_approved/review_rejected events. Optional — nil leaves the endpoints
// outbound-only (pre-slice-3 behavior).
GetReviewMessage func(ctx context.Context, messageID, agentID string) (*identity.ReviewMessageMeta, error)
ApproveInboundReview func(ctx context.Context, userID string, msg *identity.ReviewMessageMeta) *agent.OutboundError
RejectInboundReview func(ctx context.Context, userID, reason string, msg *identity.ReviewMessageMeta) *agent.OutboundError
// Review queue (account-scoped /v1/reviews). ListReviews returns all holds
// (both directions) across the user's agents; GetReviewWithContent loads one
// held message (ownership-scoped) for the detail view + approve/reject
// resolution. Both intentionally include held inbound — operator surface only.
// ListReviews is keyset-paginated on (created_at, id): the handler passes
// limit+1 to detect a further page and the after-key from the previous page's
// last row (zero afterCreatedAt = first page).
ListReviews func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterID string) ([]identity.ReviewListItem, error)
GetReviewWithContent func(ctx context.Context, userID, messageID string) (*identity.Message, error)
// ListProtectionEventsByMessage returns the per-message screening audit
// rows (gate + scan producers) behind a hold — the source of the detector
// rationale/categories shown on the review detail. Optional; when nil the
// detail omits the `protection` breakdown and clients fall back to the coded
// review_reason. Callers must have already proven ownership of the message
// (the review detail handler does, via GetReviewWithContent).
ListProtectionEventsByMessage func(ctx context.Context, messageID string) ([]identity.ProtectionEvent, error)
// SendLimit is the per-agent outbound rate limiter (mirrors
// sendLimit.AllowWithRetryAfter; key = agent id). Optional.
SendLimit func(key string) (ok bool, retryAfter time.Duration)
// PollLimit is the per-user read limiter (key = user id) and RegLimit is
// the per-IP agent-registration limiter (key = client ip). Both return
// the IETF RateLimit snapshot so the middleware can set the headers.
// Optional — nil disables that limiter on the /v1 surface.
PollLimit RateSnapshot
RegLimit RateSnapshot
// Raw capability routes sit outside Huma's authenticated middleware and use
// separate per-IP budgets so traffic to one surface cannot starve another.
// Optional — nil disables the corresponding limiter.
DownloadLimit RateSnapshot
UnsubscribeLimit RateSnapshot
// GetRepliableMessage loads a message that can be replied to or forwarded —
// either an inbound the agent received or an outbound the agent sent — as
// long as it is live (not expired) and not held/rejected in review. The
// reply/forward handlers use this so an agent can continue a thread off its
// own sent message (Gmail-style), which GetInboundMessage's direction filter
// forbids.
GetRepliableMessage func(ctx context.Context, messageID string) (*identity.Message, error)
// AttachmentStore mints/verifies short-lived attachment downloads (§6a #5).
// Native by default; when nil, the attachment endpoints are unavailable.
AttachmentStore AttachmentStore
// account
GetLimits func(ctx context.Context, userID string) (limits.Limits, error)
GetUsage func(ctx context.Context, userID string) LimitsUsageView
ExportUserData func(ctx context.Context, userID string) (*identity.UserExport, error)
// Suppression list (decision 9 / Slice 4b). Optional — nil deployments
// return 501 from the /v1/account/suppressions endpoints.
ListSuppressions func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterAddress string) ([]identity.Suppression, error)
RemoveSuppression func(ctx context.Context, userID, address string) (bool, error)
// Agent suppressions are recipient consent blocks scoped to one exact
// sending identity. Management is account-admin-only; the handler resolves
// live ownership before calling these tenant-bound store methods.
AddAgentSuppression func(ctx context.Context, userID, agentID, address, reason, source string, onAdded identity.AgentSuppressionTxHook) (identity.AgentSuppression, bool, error)
ListAgentSuppressions func(ctx context.Context, userID, agentID string, limit int, afterCreatedAt time.Time, afterAddress string) ([]identity.AgentSuppression, error)
RemoveAgentSuppression func(ctx context.Context, userID, agentID, address string) (bool, error)
// Public managed-unsubscribe capabilities. Resolve accepts only a token hash;
// the write capability accepts only the exact scope returned by that lookup,
// so the unauthenticated route cannot choose an account, agent, or recipient.
ResolveUnsubscribeToken func(ctx context.Context, tokenHash []byte) (*identity.UnsubscribeScope, error)
AddAgentSuppressionFromTokenScope func(ctx context.Context, scope identity.UnsubscribeScope, onAdded identity.AgentSuppressionTxHook) (identity.AgentSuppression, bool, error)
// Shared transaction hook for both authenticated manual creation and the
// public recipient flow; the store invokes it only for a newly inserted row.
AgentSuppressionAddedHook identity.AgentSuppressionTxHook
DeleteUserData func(ctx context.Context, user *identity.User) (*identity.DeleteUserDataResult, error)
// events (delivery log). EventQuery carries the filters + cursor
// position; the closures bind the events pool in main.
ListEvents func(ctx context.Context, q EventQuery) ([]agent.EventView, error)
GetEvent2 func(ctx context.Context, userID, eventID string) (*agent.EventView, error)
// redeliver
LoadReplayEvent func(ctx context.Context, userID, eventID string) (*agent.ReplayEvent, error)
InsertReplayDelivery func(ctx context.Context, eventID, webhookID, eventType string, messageID *string, envelope []byte) (string, error)
// EventsEnabled reflects whether the durable event log (the webhook_events
// outbox) is populated on this deployment. Now unconditional in production;
// the events handlers still gate on it so a deployment that ever disables the
// outbox returns 501 events_log_disabled from list/get/redeliver instead of
// masquerading as "no events". Webhook delivery is unaffected either way.
EventsEnabled bool
// webhooks
// CreateWebhook mirrors identity.Store.CreateWebhookIdem: when the request
// carries an Idempotency-Key the handler passes a completer that the store
// runs INSIDE the insert transaction, committing the webhook row and the
// cached replay response atomically (same-tx pattern as the send/approve
// paths). idemCompleteTx is nil for unkeyed creates.
CreateWebhook func(ctx context.Context, userID, url, description string, events []string, filters identity.WebhookFilters, idemCompleteTx identity.WebhookIdemCompleter) (*identity.Webhook, error)
// ListWebhooks is keyset-paginated on (created_at, id): the handler passes
// limit+1 to detect a further page (limit<=0 = all) and the after-key from the
// previous page's last row (zero afterCreatedAt = first page).
ListWebhooks func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterID string) ([]identity.Webhook, error)
GetWebhook func(ctx context.Context, webhookID, userID string) (*identity.Webhook, error)
UpdateWebhook func(ctx context.Context, webhookID, userID string, u identity.WebhookUpdate) (*identity.Webhook, error)
DeleteWebhook func(ctx context.Context, webhookID, userID string) error
RotateSecret func(ctx context.Context, webhookID, userID string) (string, time.Time, error)
// TestWebhookInsert schedules a synthetic delivery (subscriberStore.
// InsertPendingForTest). ListDeliveries reads the per-webhook delivery
// log (subscriberStore.ListDeliveriesByWebhook).
TestWebhookInsert func(ctx context.Context, webhookID, eventType string, envelope []byte) (string, error)
// ListDeliveries is keyset-paginated on (created_at, id): the handler passes
// limit+1 to detect a further page and the after-key from the previous page's
// last row (zero afterCreatedAt = first page). status optionally restricts to
// pending|delivered|failed.
ListDeliveries func(ctx context.Context, webhookID, status string, limit int, afterCreatedAt time.Time, afterID string) ([]webhook.SubscriberDelivery, error)
// EnqueueDelivery enqueues a River webhook_deliver job for a
// webhook_subscriber_deliveries row that was inserted directly — the /test
// endpoint and the event-redelivery API. Those two surfaces bypass the outbox
// drain (which enqueues in-tx), so without this call their rows carry no River
// job and, now that River is the sole delivery engine, would never deliver.
// Wired unconditionally in production. Optional — nil in minimal test setups
// with no River client, where a test drains delivery rows by other means.
EnqueueDelivery func(ctx context.Context, deliveryID string) error
// templates (beta). Mirror the like-named identity.Store methods; every
// lookup is scoped to the owning user (cross-user reads behave as
// not-found). GetTemplate/GetTemplateByAlias also serve the send path's
// template_id/template_alias resolution.
CreateTemplate func(ctx context.Context, userID string, in identity.TemplateCreate) (*identity.Template, error)
ListTemplates func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterID string) ([]identity.TemplateSummary, error)
GetTemplate func(ctx context.Context, templateID, userID string) (*identity.Template, error)
GetTemplateByAlias func(ctx context.Context, alias, userID string) (*identity.Template, error)
UpdateTemplate func(ctx context.Context, templateID, userID string, u identity.TemplateUpdate) (*identity.Template, error)
DeleteTemplate func(ctx context.Context, templateID, userID string) error
// API keys (account-scope management). CreateScopedAPIKey returns the
// minted key including its one-time plaintext; agentID is "" for account
// scope and a resolved agent id for agent scope.
CreateScopedAPIKey func(ctx context.Context, userID, name, scope, agentID string, expiresAt *time.Time) (*identity.APIKey, error)
ListAPIKeys func(ctx context.Context, userID string, limit int, afterCreatedAt time.Time, afterID string) ([]identity.APIKey, error)
DeleteAPIKey func(ctx context.Context, keyID, userID string) error
// domain verification
TouchDomainChecked func(ctx context.Context, domain, userID string) error
VerifyDomain func(ctx context.Context, domain, userID string) error
// EnqueueSenderProvision (decision 4 / Slice 4) schedules SES sending-
// identity provisioning for a verified domain. Called on every successful
// verify check (newly OR already verified), so POST /domains/{domain}/verify
// doubles as the forced sending re-check. Optional — nil when SES is not
// configured (dev/self-host), leaving sending_status at none (relay From).
EnqueueSenderProvision func(ctx context.Context, domain string)
// VerifyProbe runs the live DNS check for a domain's published records.
// Injected so it is fakeable in tests (the real one wraps
// agent.CheckDomainRecords).
VerifyProbe func(domain, verificationToken, dkimSelector, dkimPublicKey string) DomainCheckResult
// Deployment info surfaced by GET /v1/info.
PublicURL string
// WSHandle serves the WebSocket upgrade for an agent address (the real-
// time inbound transport). Injected so httpapi need not depend on the ws
// package; the real one is ws.Handler.ServeWithEmail.
WSHandle func(w http.ResponseWriter, r *http.Request, address string)
// Legacy is the existing gorilla/mux handler. The chi root falls back
// to it for every route not yet ported onto Huma (the strangler), so
// the service stays fully functional through the multi-sub-slice port.
Legacy http.Handler
}
Deps are the collaborators the v1 layer needs. Everything is injected so the package has no hidden globals and is straightforward to test.
type DomainCheckResult ¶
DomainCheckResult is the live-DNS diagnostic surfaced by verify.
type DomainLookup ¶
DomainLookup mirrors store.LookupDomain(domain, userID) — the create-time ownership guard.
type DomainParam ¶
type DomainParam struct {
Domain string `path:"domain"`
}
DomainParam is the path input. The domain segment is matched raw; chi/Huma URL-decode it.
type DomainView ¶
type DomainView struct {
Domain string `json:"domain"`
Verified bool `json:"verified"`
VerificationToken string `json:"verification_token"`
// DNSRecords is the unified, purpose-tagged set of records the customer must
// publish. ALL applicable records are returned at register time (they are
// deterministic), so onboarding is a single paste — sending records do not
// wait for the domain to verify.
DNSRecords []DNSRecord `json:"dns_records" nullable:"false"`
CreatedAt time.Time `json:"created_at"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
AgentCount int `json:"agent_count"`
// Sender identity (decision 4 / Slice 4). Independent of `verified`
// (inbound ownership): the async SES sending identity that lets agents
// on this domain send as their own address. This is the rollup over the
// dkim + mail_from_* records' per-record status; poll GET /domains/{domain}
// to watch it go pending → verified|failed.
SendingStatus string `` /* 152-byte string literal not displayed */
SendingError string `json:"sending_error,omitempty"`
SendingLastCheckedAt *time.Time `json:"sending_last_checked_at,omitempty"`
// SendingRamp is platform-managed and deliberately read-only. Customers can
// observe why accepted mail is waiting but cannot weaken the reputation guard.
SendingRamp SendingRampView `json:"sending_ramp"`
}
type ErrorBody ¶
type ErrorBody struct {
Code string `` /* 3510-byte string literal not displayed */
Message string `json:"message" doc:"Human-readable explanation. Not for branching — use code."`
Details any `` /* 167-byte string literal not displayed */
RequestID string `json:"request_id" doc:"Echoes the X-Request-Id response header so a failing call is greppable in logs."`
}
ErrorBody is the inner object of the envelope.
func (ErrorBody) TransformSchema ¶
TransformSchema publishes known-code metadata without closing either code or details. A generic client sees a string and open object; tooling that understands the extensions can offer stable typed views for known codes.
type ErrorEnvelope ¶
type ErrorEnvelope struct {
Err ErrorBody `json:"error"`
// contains filtered or unexported fields
}
ErrorEnvelope is the one error shape across every v1 endpoint (api-v1-redesign §4 decision 6):
{ "error": { "code": "machine_branchable", "message": "human text",
"details": {…}, "request_id": "req_…" } }
`code` is the stable, machine-branchable discriminator agents switch on; `message` is human-facing; `details` is optional structured context; and `request_id` echoes the per-request id (also on the X-Request-Id header) so a failing call is greppable in logs without correlation guesswork.
It implements huma.StatusError so it can be returned directly from a handler and is installed as the global huma.NewError constructor, which means Huma's own validation/automatic errors render in this envelope too.
func NewError ¶
func NewError(status int, code, message string) *ErrorEnvelope
NewError builds an envelope with an explicit machine-branchable code. Prefer this over the status-only helpers when the caller should be able to branch on something more specific than the HTTP status (e.g. "domain_not_verified" vs a bare 400).
func (*ErrorEnvelope) Code ¶
func (e *ErrorEnvelope) Code() string
Code returns the machine-branchable code (used by tests and middleware).
func (*ErrorEnvelope) Error ¶
func (e *ErrorEnvelope) Error() string
Error implements the error interface (huma.StatusError embeds error).
func (*ErrorEnvelope) GetStatus ¶
func (e *ErrorEnvelope) GetStatus() int
GetStatus implements huma.StatusError so Huma writes the right status.
func (*ErrorEnvelope) WithDetails ¶
func (e *ErrorEnvelope) WithDetails(details any) *ErrorEnvelope
WithDetails attaches structured details and returns the envelope for fluent construction.
func (*ErrorEnvelope) WithRetryAfter ¶
func (e *ErrorEnvelope) WithRetryAfter(seconds int) *ErrorEnvelope
WithRetryAfter records a Retry-After delay (seconds) for a handler-returned error; stampRequestID copies it into the Retry-After response header. Use it on 429s raised inside a handler (the per-agent send limiter) — the middleware-enforced limiters set the header themselves.
type EventEnvelope ¶
type EventEnvelope struct {
Type string `json:"type" doc:"Open event type; clients must tolerate unknown values."`
ID string `json:"id" doc:"Stable across retries and push channels; use it to deduplicate at-least-once delivery."`
SchemaVersion string `json:"schema_version" doc:"Open envelope-version string; the current server emits 1."`
CreatedAt time.Time `json:"created_at" format:"date-time"`
Data map[string]any `` /* 142-byte string literal not displayed */
}
EventEnvelope is the documentation schema for the push wire object shared by webhook deliveries and WebSocket frames. The runtime publisher uses webhookpub.Envelope; this map-typed data field is intentional so the published base schema stays open to unknown event types.
type EventQuery ¶
type EventQuery struct {
UserID string
Type, AgentID, ConversationID string
MessageID string
Since, Until *time.Time
CursorCreatedAt time.Time
CursorID string
Limit int
}
EventQuery is the resolved filter + cursor position passed to the events query closure.
type FieldError ¶
type FieldError struct {
Location string `` /* 222-byte string literal not displayed */
Message string `json:"message" doc:"Human-readable reason this field is invalid."`
}
FieldError is one per-field validation failure inside ValidationErrorDetails. It deliberately omits the raw offending value (Huma's ErrorDetail.Value) from the public contract so the API never echoes bad input back to the client.
type ForwardRequest ¶
type ForwardRequest struct {
To []string `` // required (MSG-3)
/* 227-byte string literal not displayed */
CC []string `` /* 232-byte string literal not displayed */
BCC []string `` /* 234-byte string literal not displayed */
Body string `json:"text" maxLength:"1048576"` // required (MSG-3); subject derived as "Fwd:"
HTMLBody string `json:"html,omitempty" maxLength:"1048576"`
ConversationID string `` /* 361-byte string literal not displayed */
ReplyTo string `` /* 282-byte string literal not displayed */
Attachments []outbound.Attachment `` /* 402-byte string literal not displayed */
Unsubscribe UnsubscribeOptions `` /* 149-byte string literal not displayed */
}
ForwardRequest mirrors the legacy forward body.
type HoldReasonView ¶
type HoldReasonView struct {
Type string `json:"type" doc:"Producer that caused the hold. Open set; tolerate unknown values. Known values: gate, scan, send, unknown."`
Code string `json:"code" doc:"Stable machine-readable hold code. Open set; tolerate unknown values."`
Summary string `json:"summary" doc:"Plain-language explanation suitable for showing directly to a reviewer."`
Category string `json:"category,omitempty" doc:"Top screening category when a scan hold has detail enrichment."`
Detail string `json:"detail,omitempty" doc:"Validated plain-language detector rationale when available."`
Confidence *float64 `` /* 150-byte string literal not displayed */
}
HoldReasonView is the product-facing explanation for why a message is in the review queue. Code is an open set; clients should render Summary rather than translating Code themselves.
type IdemStore ¶
type IdemStore interface {
Claim(ctx context.Context, userID, key, path, bodyHash string) (idempotency.ClaimResult, error)
Complete(ctx context.Context, userID, key string, resp idempotency.CachedResponse) error
// CompleteTx records the completion inside the caller's transaction — used by
// the async accept path to commit the idempotency key atomically with the
// message insert + send-job enqueue. See idempotency.Store.CompleteTx.
CompleteTx(ctx context.Context, tx pgx.Tx, userID, key string, resp idempotency.CachedResponse) error
Release(ctx context.Context, userID, key string) error
}
IdemStore is the subset of *idempotency.Store the v1 layer needs. Declared as an interface so handlers are unit-testable without Postgres.
type LimitExceededDetails ¶
type LimitExceededDetails struct {
// Resource is an OPEN set (evolving response-side vocabulary): a new
// capped resource means a new value here, and that must not break
// spec-generated clients.
Resource string `` /* 297-byte string literal not displayed */
Limit int64 `json:"limit" doc:"The cap that was hit (matches limits.max_<resource>)."`
Current int64 `json:"current" doc:"The account's usage at the time the cap was hit (matches usage.<resource>)."`
PlanCode string `json:"plan_code,omitempty" doc:"The account's plan label."`
UpgradeURL string `json:"upgrade_url,omitempty" doc:"An upgrade affordance URL, when the operator has configured one."`
}
LimitExceededDetails is the typed `error.details` payload carried by a 402 limit_exceeded response. `resource` is one of the AccountView usage/limits field stems, so a client can key the error straight to the usage/cap field: usage.<resource> for the current value and limits.max_<resource> for the cap (e.g. resource "messages_month" → usage.messages_month / limits.max_messages_month). `limit` and `current` echo the cap that was hit and the account's usage at the time. `plan_code`/`upgrade_url` are the account's plan label and any upgrade affordance the operator configured.
type LimitExceededEnvelope ¶
type LimitExceededEnvelope struct {
Err LimitExceededErrorBody `json:"error"`
}
LimitExceededEnvelope is the 402 error envelope with typed details. It is the declared schema for the 402 response on the cap-enforcing operations (create agent, register domain, send/reply/forward/test); the runtime envelope is the generic ErrorEnvelope whose `details` is populated with a LimitExceededDetails value, so the wire shape matches this schema byte-for-byte.
type LimitExceededErrorBody ¶
type LimitExceededErrorBody struct {
Code string `json:"code" enum:"limit_exceeded" doc:"Always limit_exceeded for this response."`
Message string `json:"message"`
Details LimitExceededDetails `json:"details"`
RequestID string `json:"request_id"`
}
LimitExceededErrorBody mirrors ErrorBody but with typed limit_exceeded details, so codegen surfaces a concrete detail shape for the 402 case instead of `any`.
type LimitsCapsView ¶
type LimitsUsageView ¶
type LimitsUsageView struct {
Agents int `json:"agents"`
Domains int `json:"domains"`
MessagesMonth int `json:"messages_month"`
StorageBytes int64 `` /* 228-byte string literal not displayed */
}
LimitsUsageView is the current consumption snapshot.
StorageBytes is the storage-quota accounting basis: per stored message it sums the RAW MIME byte length (raw_message — the message-level size_bytes) PLUS retained outbound draft body columns (body_text, body_html, and the draft attachments blob). These survive terminal transitions and can coexist with canonical sent MIME. A DB trigger on the messages table (migrations 016/039) maintains the total transactionally.
type ListConversationsInput ¶
type ListConversationsInput struct {
Address string `path:"email"`
Since string `query:"since" doc:"RFC3339."`
Until string `query:"until" doc:"RFC3339."`
Cursor string `` /* 136-byte string literal not displayed */
Limit int `query:"limit" minimum:"1" maximum:"100" default:"100"`
}
ListConversationsInput — since/until + cursor/limit. Cursor continuation IS supported: the handler fetches limit+1, and when there are more it keyset- encodes the last row's (last_message_at, conversation_id) into next_cursor (see handleListConversations: hasMore → EncodeCursor(conversationsCursor{...})). next_cursor is null only on the last page.
type ListDeliveriesInput ¶
type ListDeliveriesInput struct {
ID string `path:"id"`
Status string `query:"status" enum:"pending,delivered,failed"`
Cursor string `` /* 142-byte string literal not displayed */
Limit int `query:"limit" minimum:"1" maximum:"100" default:"100"`
}
ListDeliveriesInput — the per-webhook delivery log, keyset-paginated on (created_at, id) like every other v1 list. The delivery log grows unbounded on a busy webhook, so a cursor (not a fixed cap) is what keeps the whole log reachable; the limit is generous (default 100, up to 100) so a recent view is rarely more than one page. `status` restricts to pending|delivered|failed and is pinned into the cursor (a continuation must not change it).
type ListEventsInput ¶
type ListEventsInput struct {
Type string `query:"type"`
AgentID string `query:"agent_email"`
ConversationID string `query:"conversation_id"`
MessageID string `query:"message_id"`
Since string `query:"since" doc:"RFC3339."`
Until string `query:"until" doc:"RFC3339."`
Cursor string `query:"cursor"`
Limit int `query:"limit" minimum:"1" maximum:"100" default:"100"`
}
ListEventsInput — filters + the standardized cursor/limit (replacing the legacy page_size/token).
type ListMessagesInput ¶
type ListMessagesInput struct {
Address string `path:"email"`
Direction string `query:"direction" enum:"inbound,outbound,all" doc:"Defaults to inbound."`
Status string `` /* 146-byte string literal not displayed */
Sort string `query:"sort" enum:"asc,desc" doc:"Defaults to desc (newest first)."`
From string `query:"from" doc:"Case-insensitive substring match on sender."`
SubjectContains string `query:"subject_contains" doc:"Case-insensitive substring match on subject."`
ConversationID string `query:"conversation_id"`
Labels []string `query:"labels" doc:"Repeatable; AND-matched."`
Since string `query:"since" doc:"RFC3339; created_at >= since."`
Until string `query:"until" doc:"RFC3339; created_at < until."`
Cursor string `query:"cursor"`
Limit int `query:"limit" minimum:"1" maximum:"100" default:"100"`
Deleted bool `` /* 211-byte string literal not displayed */
}
ListMessagesInput is the typed query surface for the message list. Cursor pagination (cursor/limit) replaces the legacy page_size/token (§4 decision 7); the filters preserve legacy semantics.
type MXResolver ¶
MXResolver returns the effective MX hosts published for a name. Used for the create-time subdomain MX gate: a single lookup on the subdomain FQDN answers both the explicit-subdomain-MX and the wildcard-MX-on-parent cases, because a resolver synthesizes the wildcard for the queried name. Injected so unit tests mock the resolver and the httpapi layer stays off the network.
type MessageBodyView ¶
type MessageBodyView struct {
Text string `json:"text,omitempty"`
HTML string `json:"html,omitempty"`
}
MessageBodyView is the held-draft body (see MessageView.Body).
type MessageGetter ¶
MessageGetter loads a single message (with content) scoped to an agent. Mirrors store.GetMessageWithContent(messageID, agentID).
type MessageIDParam ¶
type MessageIDParam struct {
Address string `path:"email" doc:"The agent's full email address."`
MessageID string `path:"id" doc:"The message id, e.g. msg_abc123."`
}
MessageIDParam is the path input for single-message operations.
type MessageLister ¶
type MessageLister func(ctx context.Context, filter identity.MessageListFilter) ([]identity.Message, error)
MessageLister returns a filtered page of message summaries for an agent. Mirrors store.GetMessagesByAgent(filter).
type MessageParsedView ¶
type MessageParsedView struct {
// Text is the injection-reduced plain body: text/plain preferred (else
// HTML→text), quoted reply/forward chains stripped, length-capped. This is
// what an agent feeds a model by default.
Text string `json:"text"`
// Truncated is true when the length cap cut `text`.
Truncated bool `json:"truncated"`
// HTML is the decoded text/html part for display, present only when the
// message carries an HTML part. Full fidelity (NOT quote-stripped, unlike
// `text`) — render it sanitized/sandboxed; it is untrusted sender content.
// Omitted for text-only messages; `raw_message` stays the canonical copy.
HTML string `json:"html,omitempty"`
}
MessageParsedView is the parsed-body payload (see MessageView.Parsed).
type MessageSummaryView ¶
type MessageSummaryView struct {
ID string `json:"id"`
// Deliberately a CLOSED enum despite being response-side: direction is a
// binary invariant of the model, not an evolving vocabulary.
Direction string `json:"direction" enum:"inbound,outbound"`
HeaderFrom *string `` /* 182-byte string literal not displayed */
EnvelopeFrom *string `` /* 167-byte string literal not displayed */
VerifiedDomain *string `` /* 483-byte string literal not displayed */
To []string `json:"to" nullable:"false"`
CC []string `json:"cc,omitempty" nullable:"false"`
ReplyTo []string `` /* 236-byte string literal not displayed */
Recipient string `` /* 187-byte string literal not displayed */
Subject string `json:"subject"`
ConversationID string `json:"conversation_id,omitempty"`
// Status is the inbox read-state, exposed as `read_status` (MSG-1).
Status string `json:"read_status"`
HITLStatus string `` /* 541-byte string literal not displayed */
WebhookStatus string `json:"webhook_status,omitempty"`
WebhookError string `json:"webhook_error,omitempty"`
// DeliveryStatus / DeliveryDetail / SentAs are the outbound delivery
// rollup (migration 031). Outbound-only; omitted on inbound rows.
DeliveryStatus string `` /* 388-byte string literal not displayed */
DeliveryDetail string `json:"delivery_detail,omitempty"`
SentAs string `` /* 156-byte string literal not displayed */
// Flagged + FlagReason are the beta inbound ingestion verdict. They remain in
// list projections so polling agents can identify delivered flag outcomes
// without a per-message drill-down.
Flagged bool `json:"flagged,omitempty"`
FlagReason string `json:"flag_reason,omitempty"`
// SizeBytes is the RAW MIME byte length of the whole stored message —
// same semantics as MessageView.SizeBytes (see there for the full note,
// including its role as the dominant term of storage-quota accounting).
SizeBytes int `` /* 313-byte string literal not displayed */
Labels []string `json:"labels" nullable:"false"`
// CreatedAt is the keyset pagination ordering key, emitted at full RFC3339Nano
// precision (time.Time) so sub-second ordering is visible on the wire.
CreatedAt time.Time `json:"created_at" format:"date-time"`
// DeletedAt marks a message in the trash — set on rows of the deleted=true
// list view, omitted on live messages. See MessageView.DeletedAt.
DeletedAt *time.Time `` /* 284-byte string literal not displayed */
}
MessageSummaryView is the lightweight list representation. It mirrors the legacy messageSummary json shape field-for-field (Slice 1 keeps the item shape; only the *pagination envelope* changes to the standardized items/next_cursor — §4 decision 7). Replicated here rather than imported from the legacy agent package so the new layer carries no backwards dependency on the surface it replaces; it moves home when legacy is deleted at the 1Z cutover.
type MessageTrashOp ¶
MessageTrashOp mirrors the store's per-message trash mutations (SoftDeleteMessage / RestoreMessage / PurgeMessage): scoped to (messageID, agentID), returning the sentinel errors ErrMessageHeld / ErrNotInTrash / ErrMessageNotFound for the handler to map.
type MessageView ¶
type MessageView struct {
ID string `json:"id"`
HeaderFrom *string `` /* 182-byte string literal not displayed */
EnvelopeFrom *string `` /* 167-byte string literal not displayed */
VerifiedDomain *string `` /* 554-byte string literal not displayed */
Authentication *emailauth.Authentication `` /* 326-byte string literal not displayed */
To []string `json:"to" nullable:"false"`
CC []string `json:"cc" nullable:"false"`
ReplyTo []string `` /* 257-byte string literal not displayed */
Recipient string `` /* 187-byte string literal not displayed */
Subject string `json:"subject"`
ConversationID string `json:"conversation_id"`
// Direction (inbound|outbound) — mirrors MessageSummaryView so a client
// fetching a single message keeps the full trust-axis context (review F1).
// Deliberately a CLOSED enum despite being response-side: direction is a
// binary invariant of the model, not an evolving vocabulary.
Direction string `json:"direction" enum:"inbound,outbound"`
// Status is the inbox read-state (unread|read; "" for outbound). Exposed as
// `read_status` (MSG-1) to disambiguate from hitl_status/delivery_status/
// webhook_status — the conflation that caused bug B2. Left open (not an enum)
// because outbound rows carry "".
Status string `json:"read_status"`
// HITLStatus is the review-hold lifecycle (e.g. pending_review) — outbound
// only, mirroring MessageSummaryView. Exposed as `review_status` (the holds
// vocabulary unified on `review` in migration 044). Distinct from read_status,
// delivery_status, and webhook_status (each a separate axis).
HITLStatus string `` /* 541-byte string literal not displayed */
// WebhookStatus / WebhookError mirror MessageSummaryView so the detail view
// is a strict superset of the list item (a client fetching one message keeps
// the webhook delivery context). Apply to both directions; omitempty hides
// the empty case.
WebhookStatus string `json:"webhook_status,omitempty"`
WebhookError string `json:"webhook_error,omitempty"`
// SizeBytes is the RAW MIME byte length of the whole stored message
// (octet length of raw_message) — headers + bodies + encoded attachments as
// transported. Mirrors MessageSummaryView. Distinct from the per-attachment
// size_bytes in attachments[], which is the DECODED payload size of one
// attachment. This raw length is also the dominant term of storage-quota
// accounting: usage.storage_bytes sums raw_message plus any retained
// held-draft body columns per message (see AccountView).
SizeBytes int `` /* 324-byte string literal not displayed */
// DeliveryStatus is the outbound delivery rollup (migration 031:
// 'sent', 'delivered', 'bounced', …) — the worst recipient status by
// precedence. Outbound-only; omitted on inbound messages.
DeliveryStatus string `` /* 388-byte string literal not displayed */
// DeliveryDetail is the human-readable diagnostic for the delivery
// rollup (e.g. bounce sub-type / SMTP response). Outbound-only.
DeliveryDetail string `json:"delivery_detail,omitempty"`
// SentAs is the From identity actually used at relay accept time.
// Outbound-only; omitted on inbound messages.
SentAs string `` /* 156-byte string literal not displayed */
// Flagged + FlagReason carry the beta inbound ingestion verdict: true when
// the agent's inbound-policy gate flagged this message on arrival while still
// delivering it. Polling agents need this signal because no review item is
// created for the flag action.
Flagged bool `json:"flagged,omitempty"`
FlagReason string `json:"flag_reason,omitempty"`
// HoldReason is populated only by GET /v1/reviews/{id}. The shared
// messageViewFromIdentity constructor deliberately leaves it nil so review
// context never leaks onto agent-facing message APIs.
HoldReason *HoldReasonView `json:"hold_reason,omitempty"`
// Protection is the per-producer screening breakdown behind the hold (the
// detector categories + rationale that explain hold_reason). Review surface
// only; populated by the review-detail handler, never on the agent /messages
// path. Beta.
Protection []ProtectionFindingView `` /* 134-byte string literal not displayed */
Labels []string `json:"labels" nullable:"false"`
// CreatedAt is emitted as a full-precision RFC3339Nano date-time (time.Time),
// consistent with every other timestamp in the surface. This is the keyset
// pagination ORDERING key, so it must NOT be truncated to whole seconds —
// two messages in the same second would otherwise be indistinguishable on the
// wire even though the cursor orders them at finer granularity.
CreatedAt time.Time `json:"created_at" format:"date-time"`
// DeletedAt marks a message in the trash (soft-deleted): set when it was
// moved there, omitted on live messages. Trashed messages appear only in
// the deleted=true list view and this single-message get; they are purged
// ~30 days after deletion (docs/design/trash-soft-delete.md).
DeletedAt *time.Time `` /* 284-byte string literal not displayed */
RawMessage []byte `` /* 278-byte string literal not displayed */
// Parsed is the derived view (decision 9 / Slice 4b-3): the raw message
// rendered to text (`text`, quoted chains stripped + length-capped, for the
// agent to feed a model by default) plus the decoded HTML part (`html`, for
// display). Present on any message carrying raw MIME — inbound and sent
// outbound. A CONVENIENCE — when raw_message is non-null, it is canonical;
// the security decision is made on `auth` + provenance, never on this derived
// body. Held outbound drafts have raw_message=null and use Body below.
Parsed *MessageParsedView `json:"parsed,omitempty"`
// Body is the mutable draft body for a held outbound message
// (status=pending_review), which has no raw_message yet. This is the
// second representation the unified read exposes (decision 9): held drafts
// carry body_text/body_html, sent/inbound carry raw_message. Omitted when
// empty (sent/inbound rows).
Body *MessageBodyView `json:"body,omitempty"`
// Attachments is per-attachment METADATA (never bytes) parsed server-side
// from raw_message — the authoritative, stable attachment index (§6a #5).
// Fetch the bytes via GET …/messages/{id}/attachments/{index}. Always
// present (empty when none); held drafts (no raw_message) carry [].
Attachments []AttachmentMetaView `json:"attachments" nullable:"false"`
}
MessageView is the full single-message representation: a strict superset of MessageSummaryView plus body/parsed/raw_message. The inbox read-state is exposed as `read_status` (MSG-1); the four status axes are read_status / hitl_status / delivery_status / webhook_status.
type Page ¶
type Page[T any] struct { Items []T `json:"items" nullable:"false"` NextCursor *string `json:"next_cursor"` }
Page is the one list-response shape across every v1 collection (api-v1-redesign §4 decision 7): an items array plus an opaque continuation cursor that is null on the last page.
{ "items": [...], "next_cursor": "…" | null }
It is generic over the item view type so each resource reuses the same envelope without redefining it.
type PageParams ¶
type PageParams struct {
Cursor string `` /* 142-byte string literal not displayed */
Limit int `query:"limit" minimum:"1" maximum:"100" default:"100" doc:"Maximum number of items to return (1-100)."`
}
PageParams is the embeddable Huma input fragment for cursor pagination. Every list operation embeds it so `cursor` + `limit` are declared, typed, and validated identically across the surface.
type PayloadTooLargeDetails ¶
type PayloadTooLargeDetails struct {
// Scope is an OPEN set (evolving response-side vocabulary): a new byte
// budget (e.g. a future batch or template cap) means a new value here,
// and that must not break spec-generated clients.
Scope string `` /* 227-byte string literal not displayed */
ActualBytes int64 `` /* 195-byte string literal not displayed */
MaxBytes int64 `json:"max_bytes" minimum:"1" doc:"Maximum bytes accepted for this scope."`
Filename string `json:"filename,omitempty" doc:"Attachment filename when scope is attachment."`
}
type PrincipalAuthenticator ¶
PrincipalAuthenticator is the scope-aware seam (Slice 5a): the same single auth path, but returning the full principal (user + credential scope + bound agent) so the v1 handlers can enforce the hard scope ceiling (design §5). When set it supersedes Authenticator; when nil the server wraps Authenticator and treats every caller as account-scoped (pre-Slice-5a behavior).
type ProtectionConfigRequest ¶
type ProtectionConfigRequest struct {
Inbound ProtectionDirectionRequest `json:"inbound"`
Outbound ProtectionDirectionRequest `json:"outbound"`
Holds ProtectionHoldsRequest `json:"holds"`
}
ProtectionConfigRequest is the PUT body (full replace). The three top-level keys are required (no silent section reset); leaves are optional and fill from defaults.
type ProtectionConfigView ¶
type ProtectionConfigView struct {
Inbound ProtectionDirectionView `json:"inbound"`
Outbound ProtectionDirectionView `json:"outbound"`
Holds ProtectionHoldsView `json:"holds"`
}
ProtectionConfigView is the GET (and PUT echo) RESPONSE. The PUT body is the separate ProtectionConfigRequest below — see its comment for why the two are distinct types despite the identical shape.
type ProtectionDirectionRequest ¶
type ProtectionDirectionRequest struct {
Gate ProtectionGateRequest `json:"gate"`
Scan ProtectionScanRequest `json:"scan"`
}
ProtectionDirectionRequest mirrors ProtectionDirectionView for the PUT body.
type ProtectionDirectionView ¶
type ProtectionDirectionView struct {
Gate ProtectionGateView `json:"gate"`
Scan ProtectionScanView `json:"scan"`
}
ProtectionDirectionView pairs the gate and scan for one direction.
type ProtectionFindingView ¶
type ProtectionFindingView struct {
Source string `json:"source" doc:"Which producer flagged the message. Open set; tolerate unknown values. Known values: gate, scan."`
Action string `json:"action,omitempty" doc:"Applied action for this finding. Open set. Known values: flag, review, block."`
Detector string `json:"detector,omitempty" doc:"Content-scan detector(s) that contributed (scan findings only), e.g. \"gemini\"."`
Score *float64 `json:"score,omitempty" doc:"Aggregate content-scan score 0..1 (scan findings only)."`
Categories []ThreatCategoryView `json:"categories,omitempty" doc:"Detected threat categories, highest-confidence first (scan findings only)."`
// Summary is the detector's short natural-language rationale (e.g. "instructs
// the agent to wire funds"). Curated from the per-detector verdict — the raw
// provider payload is never exposed.
Summary string `json:"summary,omitempty" doc:"Short natural-language rationale for a scan finding (curated; never the raw provider payload)."`
}
ProtectionFindingView is one screening producer's verdict on a held message — the review-detail breakdown behind hold_reason. A scan finding (source=scan) carries the content-detector's categories + one-line rationale; a gate finding (source=scan's counterpart, source=gate) carries only its source + action today (the address that tripped the policy is intentionally not surfaced here — the reviewer already sees sender/recipient on the message). Review surface only (GET /v1/reviews/{id}); the agent /messages read paths never return holds. Beta: the shape may change.
type ProtectionGateRequest ¶
type ProtectionGateRequest struct {
Policy string `` /* 403-byte string literal not displayed */
Allowlist []string `` /* 349-byte string literal not displayed */
Action string `` /* 146-byte string literal not displayed */
}
ProtectionGateRequest / ProtectionScanRequest / ProtectionDirectionRequest / ProtectionHoldsRequest / ProtectionConfigRequest mirror the *View shapes field-for-field as the PUT body. They are dedicated INPUT types (not the Views) because the spec's forward-compat stance is asymmetric: request schemas stay `additionalProperties: false` (strict validation — an unknown key is a 422, not a silent no-op on a security posture), while response schemas are open (`additionalProperties: true`) so clients tolerate additive fields. One shared schema cannot carry both; the stability pass in stability.go panics if a schema is reachable from both sides. Keep the validation tags here in lockstep with the View tags above — except the enum tags, which live ONLY on these request types: the Views document the same vocabularies as open sets (response-side vocabularies that can evolve are open; see docs/api.md "Versioning & stability"), while an unknown value a client SENDS is still validated and rejected here.
type ProtectionGateView ¶
type ProtectionGateView struct {
// Policy and Action are OPEN sets on this response view (evolving config
// vocabularies — a new gate posture or disposition must not break
// spec-generated clients); the mirroring ProtectionGateRequest keeps the
// closed enums, which is where validation belongs.
Policy string `` /* 513-byte string literal not displayed */
Allowlist []string `` /* 349-byte string literal not displayed */
Action string `` /* 256-byte string literal not displayed */
}
ProtectionGateView is one direction's trust gate: who may send, and what a non-match does. policy is a monotonic trust ladder (open < domain < allowlist). Leaves are optional (omitempty) with defaults, so a caller may send an empty gate/scan object and get the safe-permissive default. The three TOP-LEVEL keys (inbound/outbound/holds) stay required (design D3) — a missing section is a 422, not a silent reset.
INBOUND gate identity note (#299): the inbound allowlist/domain gate matches on the message's From address, which only carries a specific per-agent identity for senders on a sending-verified domain. Mail from agents NOT yet sending-verified is relayed under the shared "via e2a" address (internal/outbound/sender.go) — it authenticates but is the same address for every such agent. The relay treats that shared sender as unresolvable (see senderResolvable), so it can never satisfy an allowlist/domain gate and is flagged (fail closed); under "open" it still passes. Net: per-agent inbound allowlisting is reliable for sending-verified senders; unverified intra-system senders are uniformly gated, not silently admitted.
Inbound allowlist/domain gates fail closed unless DMARC passes, then match the aligned RFC 5322 From address. Open policy remains intentionally ungated.
type ProtectionHoldsRequest ¶
type ProtectionHoldsRequest struct {
TTLSeconds int `json:"ttl_seconds,omitempty" minimum:"0" default:"604800" doc:"How long a held item waits before its on_expiry action fires."`
OnExpiry string `json:"on_expiry,omitempty" enum:"approve,reject" default:"reject" doc:"What happens to a held item when its TTL expires."`
SuppressNotifications bool `` /* 135-byte string literal not displayed */
}
ProtectionHoldsRequest mirrors ProtectionHoldsView for the PUT body.
type ProtectionHoldsView ¶
type ProtectionHoldsView struct {
TTLSeconds int `json:"ttl_seconds,omitempty" minimum:"0" default:"604800" doc:"How long a held item waits before its on_expiry action fires."`
// OnExpiry is an OPEN set on this response view: today's values happen to
// be two, but expiry disposition is an evolving config vocabulary (e.g. a
// future extend/escalate), not a binary invariant of the model like
// message direction. ProtectionHoldsRequest keeps the closed enum.
OnExpiry string `` /* 230-byte string literal not displayed */
SuppressNotifications bool `` /* 135-byte string literal not displayed */
}
ProtectionHoldsView is the shared review-queue mechanism for held items.
type ProtectionScanRequest ¶
type ProtectionScanRequest struct {
Sensitivity string `` /* 156-byte string literal not displayed */
}
ProtectionScanRequest mirrors ProtectionScanView for the PUT body.
type ProtectionScanView ¶
type ProtectionScanView struct {
// Sensitivity is an OPEN set on this response view (evolving vocabulary —
// a new level must not break clients); ProtectionScanRequest keeps the
// closed enum for validation.
Sensitivity string `` /* 267-byte string literal not displayed */
}
ProtectionScanView is one direction's content scan, as a semantic sensitivity level. off disables it; low|medium|high tune how aggressively flagged content is held/blocked.
type RateLimitedDetails ¶
type RateLimitedDetails struct {
RetryAfterSeconds int `json:"retry_after_seconds" doc:"Seconds to wait before retrying; mirrors the Retry-After response header. Always ≥ 1."`
}
RateLimitedDetails is the typed `error.details` payload carried by a 429 rate_limited response. `retry_after_seconds` is the seconds a client should wait before retrying — it mirrors the Retry-After response header, so a client that can only read the body still gets the backoff hint.
This is the THROUGHPUT/request-RATE arm of the contract, distinct from the 402 limit_exceeded (stock/flow QUOTA) arm: a 429 is a short-lived, retry-able signal (wait retry_after_seconds and the same request succeeds), whereas a 402 is a persistent cap that a retry alone will not clear (see LimitExceededDetails). Clients MUST branch on the HTTP status: 429 → back off and retry; 402 → surface a quota/upgrade path, do not hammer-retry.
type RateLimitedEnvelope ¶
type RateLimitedEnvelope struct {
Err RateLimitedErrorBody `json:"error"`
}
RateLimitedEnvelope is the 429 error envelope with typed details. It is the declared schema for the 429 response on the throughput-limited write operations (send/reply/forward/test, create agent, approve review); the runtime envelope is the generic ErrorEnvelope whose `details` is populated with a RateLimitedDetails value (or the equivalent map), so the wire shape matches this schema byte-for-byte. It is the request-RATE counterpart to the 402 LimitExceededEnvelope (stock/flow QUOTA) — the two are the permanent GA split clients branch on by HTTP status.
type RateLimitedErrorBody ¶
type RateLimitedErrorBody struct {
Code string `json:"code" enum:"rate_limited" doc:"Always rate_limited for this response."`
Message string `json:"message"`
Details RateLimitedDetails `json:"details"`
RequestID string `json:"request_id"`
}
RateLimitedErrorBody mirrors ErrorBody but with typed rate_limited details, so codegen surfaces a concrete detail shape for the 429 case instead of `any`.
type RateSnapshot ¶
type RateSnapshot func(key string) (ok bool, retryAfter time.Duration, limit, remaining, resetSeconds int)
RateSnapshot records an attempt for key and reports whether it is allowed along with the IETF RateLimit header values (quota, remaining-after-this, seconds-until-reset) and the Retry-After delay when blocked. It mirrors ratelimit.Limiter.AllowSnapshot so httpapi shares the exact buckets the legacy surface uses without importing the limiter directly.
type RedeliverDelivery ¶
type RedeliverDelivery struct {
WebhookID string `json:"webhook_id"`
DeliveryID string `json:"delivery_id,omitempty"`
// Status is "pending" when the replay was scheduled, or "skipped" when this
// subscriber's delivery could not be enqueued (see Reason).
Status string `` /* 140-byte string literal not displayed */
Reason string `json:"reason,omitempty"`
}
type RedeliverEventInput ¶
type RedeliverEventInput struct {
ID string `path:"id"`
Body RedeliverEventRequest
}
type RedeliverEventRequest ¶
type RedeliverEventRequest struct {
WebhookID string `json:"webhook_id,omitempty"`
}
RedeliverEventRequest is the redeliver body (WH-6 naming).
func (*RedeliverEventRequest) UnmarshalJSON ¶
func (r *RedeliverEventRequest) UnmarshalJSON(data []byte) error
UnmarshalJSON preserves the contract distinction between an omitted optional webhook_id (bulk fan-out) and an explicitly null, non-nullable webhook_id.
type RedeliverView ¶
type RedeliverView struct {
DeliveryID string `json:"delivery_id,omitempty"`
EventID string `json:"event_id"`
WebhookID string `json:"webhook_id,omitempty"`
// Status is "pending" for a single-webhook replay (one scheduled delivery)
// or "scheduled" for a bulk fan-out (see Deliveries for per-subscriber state).
Status string `` /* 127-byte string literal not displayed */
Deliveries []RedeliverDelivery `json:"deliveries,omitempty" nullable:"false"`
}
RedeliverView mirrors the legacy redeliverResponse.
type RegisterDomainRequest ¶
type RegisterDomainRequest struct {
Domain string `json:"domain"`
}
RegisterDomainRequest is the create-domain body. `domain` is required (D-4): it is the only field, so leaving it optional generated an SDK signature that compiled with no domain and failed at runtime.
type RejectRequest ¶
type RejectRequest struct {
Reason string `` /* 197-byte string literal not displayed */
}
RejectRequest is the reject body (MSG-10, was the inline RejectInputBody).
type RejectResultView ¶
type RejectResultView struct {
Status string `json:"status"`
MessageID string `json:"message_id"`
RejectionReason string `json:"rejection_reason,omitempty"`
}
RejectResultView is the reject response. Reject is not a send, so it keeps its own shape (status + rejection_reason).
type RenderedTemplateView ¶
type RenderedTemplateView struct {
Subject string `json:"subject"`
Body string `json:"text"`
HTMLBody string `json:"html,omitempty"`
}
RenderedTemplateView is the rendered preview (present only when valid).
type ReplyRequest ¶
type ReplyRequest struct {
Body string `json:"text" maxLength:"1048576"` // required (MSG-3); to/subject derived from the original
HTMLBody string `json:"html,omitempty" maxLength:"1048576"`
ReplyAll bool `json:"reply_all,omitempty"`
CC []string `` /* 249-byte string literal not displayed */
BCC []string `` /* 251-byte string literal not displayed */
ConversationID string `` /* 361-byte string literal not displayed */
ReplyTo string `` /* 282-byte string literal not displayed */
Attachments []outbound.Attachment `` /* 275-byte string literal not displayed */
Unsubscribe UnsubscribeOptions `` /* 149-byte string literal not displayed */
}
ReplyRequest mirrors the legacy reply body.
type RetryAfterDetails ¶
type RetryAfterDetails struct {
RetryAfterSeconds int `json:"retry_after_seconds" minimum:"1" doc:"Seconds to wait before retrying; mirrors the Retry-After response header."`
}
RetryAfterDetails is the open transient-availability retry hint used by limits_unavailable. It intentionally matches the rate-limit field name so generic clients can share one backoff parser across 429 and 503 responses.
type ReviewView ¶
type ReviewView struct {
ID string `` /* 229-byte string literal not displayed */
Agent string `json:"agent_email" doc:"The inbox (agent address) the held message belongs to."`
// Direction: outbound = a draft awaiting send approval; inbound = a screened
// incoming message awaiting release. Deliberately a CLOSED enum despite
// being response-side: direction is a binary invariant of the model, not
// an evolving vocabulary.
Direction string `json:"direction" enum:"inbound,outbound"`
HeaderFrom *string `` /* 182-byte string literal not displayed */
EnvelopeFrom *string `` /* 167-byte string literal not displayed */
VerifiedDomain *string `` /* 483-byte string literal not displayed */
To []string `json:"to" nullable:"false"`
Subject string `json:"subject"`
ConversationID string `json:"conversation_id,omitempty"`
ReviewStatus string `` /* 154-byte string literal not displayed */
Flagged bool `json:"flagged,omitempty"`
FlagReason string `json:"flag_reason,omitempty"`
HoldReason *HoldReasonView `` /* 168-byte string literal not displayed */
CreatedAt time.Time `json:"created_at"`
}
ReviewView is one item in the review queue — non-secret summary of a held message of either direction.
type SendEmailRequest ¶
type SendEmailRequest struct {
To []string `` /* 227-byte string literal not displayed */
CC []string `` /* 232-byte string literal not displayed */
BCC []string `` /* 234-byte string literal not displayed */
Subject string `` /* 163-byte string literal not displayed */
Body string `` /* 171-byte string literal not displayed */
HTMLBody string `json:"html,omitempty" maxLength:"1048576" doc:"Literal HTML body. Mutually exclusive with template_id/template_alias."`
TemplateID string `` /* 280-byte string literal not displayed */
TemplateAlias string `` /* 264-byte string literal not displayed */
TemplateData TemplateData `` /* 249-byte string literal not displayed */
ConversationID string `` /* 352-byte string literal not displayed */
ReplyTo string `` /* 320-byte string literal not displayed */
Attachments []outbound.Attachment `` /* 275-byte string literal not displayed */
Unsubscribe UnsubscribeOptions `` /* 149-byte string literal not displayed */
}
SendEmailRequest is the new-thread send body. `to` is required (RFC 5321 requires ≥1 recipient; From/Date are server-set). Content comes in one of two mutually exclusive shapes: literal subject + text (+ optional html) — both required at the handler for a usable new email (MSG-3) — or a template reference (template_id XOR template_alias, + template_data), which the server renders into subject/text/html before any further processing. subject/text moved from schema-required to handler-enforced so the template shape can omit them.
type SendResultView ¶
type SendResultView struct {
// review_approved is the inbound-release outcome of POST .../approve (an
// inbound hold released to the agent's inbox — no send). sent/pending_review
// are the send/outbound-approve outcomes.
Status string `` /* 391-byte string literal not displayed */
MessageID string `json:"message_id"`
ProviderMessageID string `` /* 182-byte string literal not displayed */
SentAs string `json:"sent_as,omitempty" doc:"From identity used. Open set; tolerate unknown values. Known values: own_address, relay."`
Method string `json:"method,omitempty" doc:"Send transport. Open set; tolerate unknown values. Known values: smtp, loopback."`
ApprovalExpiresAt *time.Time `json:"approval_expires_at,omitempty"`
// Edited is set only by approve (true/false = did the reviewer edit the
// draft before sending); omitted on the plain send path.
Edited *bool `json:"edited,omitempty"`
}
SendResultView is the single outbound result for send/reply/forward/approve/ test (MSG-9). Per scenario:
- sent: status="sent" + message_id (the e2a msg_ id) + provider_message_id (SES id) + sent_as + method.
- held: status="pending_review" + message_id + approval_expires_at.
- approved+sent: the "sent" set + edited (reviewer edited the draft).
message_id is always the e2a message id (GET-able), never the provider id — the SES id is provider_message_id. `reject` keeps its own RejectResultView (it is not a send).
type SendingRampView ¶
type SendingRampView struct {
Status string `json:"status" doc:"Platform-managed sending-ramp state. Open set; known values: inactive, ramping, complete, exempt."`
DailyRecipientLimit int `json:"daily_recipient_limit" doc:"Current UTC-day recipient allowance. Zero means no ramp cap applies."`
RecipientsUsedToday int `` /* 150-byte string literal not displayed */
ResetsAt *time.Time `json:"resets_at,omitempty"`
ActiveDays int `json:"active_days" doc:"UTC days that reached the provider-accepted volume threshold."`
RampDays int `json:"ramp_days"`
EstimatedCompletionAt *time.Time `` /* 157-byte string literal not displayed */
}
type Server ¶
Server is the v1 HTTP surface: a chi root router with the Huma API mounted on it and the legacy handler wired as the fallback.
func New ¶
New builds the v1 server. It installs the e2a error envelope globally, stands up the Huma API on a chi router under the `/v1` documentation paths, registers the ported operations, and points chi's not-found/ method-not-allowed handlers at the legacy surface.
func (*Server) OpenAPIYAML ¶
OpenAPIYAML renders the generated spec as YAML. Used by the codegen step and the drift test — the spec is emitted from the live handlers, never hand-authored.
type StarterTemplateAliasParam ¶
type StarterTemplateAliasParam struct {
Alias string `path:"alias" doc:"The starter template's alias, e.g. welcome."`
}
StarterTemplateAliasParam is the path input for single-starter ops.
type StarterTemplateDetailView ¶
type StarterTemplateDetailView struct {
StarterTemplateView
Body string `json:"text" doc:"The plain-text part's template source."`
HTMLBody string `json:"html" doc:"The HTML part's template source."`
}
StarterTemplateDetailView is the single-starter shape: the list fields (flattened via embedding) plus the full body sources.
type StarterTemplateVariableView ¶
type StarterTemplateVariableView struct {
Name string `json:"name"`
Required bool `json:"required" doc:"Required variables should always be supplied; optional ones render as empty strings when absent."`
Raw bool `` /* 141-byte string literal not displayed */
Description string `json:"description"`
Example string `json:"example" doc:"A realistic sample value, usable as template_data for previews."`
}
StarterTemplateVariableView describes one interpolation slot a starter template declares.
type StarterTemplateView ¶
type StarterTemplateView struct {
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Subject string `json:"subject" doc:"Subject template source ({{variable}} interpolation)."`
Variables []StarterTemplateVariableView `json:"variables" nullable:"false"`
}
StarterTemplateView is one starter template in the list — catalog metadata only. The (large) body sources are returned by the get-by-alias endpoint.
type SuppressionView ¶
type SuppressionView struct {
Address string `json:"address"`
Reason string `json:"reason,omitempty"`
Source string `json:"source"` // bounce | complaint | manual
SourceMessageID string `json:"source_message_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
SuppressionView is the wire shape of one (user, address) entry on the per-tenant suppression list — the HTTP DTO over the identity.Suppression storage model. Field-for-field identical JSON; mapped via suppressionView().
type TemplateData ¶
TemplateData is a free-form JSON object carrying template variables (template_data on send, test_data on validate). It decodes with json.Decoder.UseNumber so numeric values arrive as json.Number and render digit-exact — plain encoding/json would decode every number as float64, corrupting integers beyond 2^53 (123456789012345678 → …680). The OpenAPI schema is unchanged: reflection sees an ordinary map → free-form object.
func (*TemplateData) UnmarshalJSON ¶
func (d *TemplateData) UnmarshalJSON(b []byte) error
type TemplateIDParam ¶
type TemplateIDParam struct {
ID string `path:"id"`
}
TemplateIDParam is the path input for single-template read/update ops.
type TemplatePartError ¶
type TemplatePartError struct {
Part string `json:"part" doc:"Which part failed. Known values: subject, text, html."`
Message string `json:"message"`
}
TemplatePartError is one per-part validation failure.
type TemplateSummaryView ¶
type TemplateSummaryView struct {
ID string `json:"id"`
Name string `json:"name"`
Alias string `json:"alias,omitempty" doc:"Optional per-user unique handle usable as template_alias on send."`
Subject string `json:"subject"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
}
TemplateSummaryView is the list-item shape: metadata only, NO body/ html_body sources (a full library of maximal templates would ship megabytes per list call; every list consumer needs metadata). Fetch one by id for the sources — same split as the starter-templates list/detail.
type TemplateView ¶
type TemplateView struct {
ID string `json:"id"`
Name string `json:"name"`
Alias string `json:"alias,omitempty" doc:"Optional per-user unique handle usable as template_alias on send."`
Subject string `json:"subject"`
Body string `json:"text" doc:"The plain-text part's template source."`
HTMLBody string `json:"html,omitempty" doc:"The optional HTML part's template source."`
// Read-only starter provenance, set only on from_starter creates.
FromStarterAlias string `` /* 214-byte string literal not displayed */
FromStarterVersion string `` /* 215-byte string literal not displayed */
CreatedAt time.Time `json:"created_at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
}
TemplateView is the full template resource (create/get/update responses; the list returns TemplateSummaryView).
type TestWebhookRequest ¶
type TestWebhookRequest struct {
Event string `` /* 304-byte string literal not displayed */
Data map[string]any `json:"data,omitempty"`
}
TestWebhookRequest mirrors the legacy body.
type TestWebhookResponse ¶
type TestWebhookResponse struct {
DeliveryID string `json:"delivery_id"`
}
TestWebhookResponse is the test-delivery result (WH-6 naming).
type ThreatCategoryView ¶
ThreatCategoryView is a single detected threat class + its confidence.
type UnsubscribeOptions ¶
type UnsubscribeOptions struct {
Mode string `` /* 141-byte string literal not displayed */
Present bool `json:"-"`
}
UnsubscribeOptions is the beta per-message opt-in to e2a-managed unsubscribe handling. It is presence-aware because omission is the compatibility path: it means only that managed unsubscribe was not requested and does not classify the message as transactional. This type may change before stable.
func (*UnsubscribeOptions) UnmarshalJSON ¶
func (o *UnsubscribeOptions) UnmarshalJSON(data []byte) error
type UpdateAgentRequest ¶
type UpdateAgentRequest struct {
Name *string `` /* 201-byte string literal not displayed */
}
UpdateAgentRequest is the /v1 agent PATCH body. The per-agent screening/HITL config moved to the /v1/agents/{email}/protection sub-resource (design 2026-06-22), so the only mutable field left on the agent itself is the display name. Pointer so absent != "" (an empty name is a valid clear).
type UpdateMessageRequest ¶
type UpdateMessageRequest struct {
AddLabels []string `json:"add_labels,omitempty" nullable:"false"`
RemoveLabels []string `json:"remove_labels,omitempty" nullable:"false"`
}
UpdateMessageRequest is the labels-delta body for PATCH …/messages/{id}. A label in both add and remove is removed (remove wins, per the store).
type UpdateMessageResultView ¶
type UpdateMessageResultView struct {
MessageID string `json:"message_id"`
Labels []string `json:"labels" nullable:"false"`
}
UpdateMessageResultView echoes the post-update label set so callers can reflect state without a follow-up fetch.
type UpdateTemplateRequest ¶
type UpdateTemplateRequest struct {
Name *string `json:"name,omitempty"`
Alias *string `json:"alias,omitempty" doc:"Set to \"\" to clear the alias."`
Subject *string `json:"subject,omitempty"`
Body *string `json:"text,omitempty"`
HTMLBody *string `json:"html,omitempty" doc:"Set to \"\" to remove the HTML part."`
}
UpdateTemplateRequest is the PATCH body — pointer fields so absent != zero. Setting alias or html to "" clears it. Changed parts are re-parsed.
type UpdateWebhookRequest ¶
type UpdateWebhookRequest struct {
URL *string `json:"url,omitempty" doc:"When present, must be a non-empty webhook delivery URL; omit to leave the URL unchanged."`
Events *[]string `` /* 485-byte string literal not displayed */
Filters *WebhookFiltersRequest `json:"filters,omitempty"`
Description *string `json:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
UpdateWebhookRequest mirrors the legacy PATCH body — pointer fields so absent != zero; url/events/filters are full-replace when present.
type ValidateTemplateRequest ¶
type ValidateTemplateRequest struct {
Subject string `json:"subject,omitempty"`
Body string `json:"text,omitempty"`
HTMLBody string `json:"html,omitempty"`
TestData TemplateData `json:"test_data,omitempty" doc:"Sample template_data to render the preview with. Missing variables render as empty strings."`
}
ValidateTemplateRequest carries template source to dry-run. Parts may be empty (an empty part parses trivially and renders empty).
type ValidateTemplateResponse ¶
type ValidateTemplateResponse struct {
Valid bool `json:"valid"`
Errors []TemplatePartError `json:"errors" nullable:"false"`
Rendered *RenderedTemplateView `json:"rendered,omitempty" doc:"Rendered preview against test_data (or empty data). Present only when valid."`
SuggestedData map[string]any `` /* 235-byte string literal not displayed */
}
ValidateTemplateResponse is the dry-run report.
type ValidationErrorDetails ¶
type ValidationErrorDetails struct {
Fields []FieldError `` /* 145-byte string literal not displayed */
}
ValidationErrorDetails is the typed error.details payload for the invalid_request code: the list of per-field validation failures that made the request invalid. It is the validation arm of the polymorphic-by-code details contract (other codes carry their own typed detail object — e.g. a limit_exceeded detail for the 402 quota path), so codegen emits a concrete model clients can read after branching on code == "invalid_request".
type VerifyDomainView ¶
type VerifyDomainView struct {
Domain string `json:"domain"`
Verified bool `json:"verified"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
MX string `` /* 591-byte string literal not displayed */
SPF string `` /* 719-byte string literal not displayed */
DKIM string `` /* 1071-byte string literal not displayed */
}
VerifyDomainView mirrors the legacy VerifyDomainResponse.
The mx/spf/dkim fields report the LIVE DNS probe outcome of this verification attempt in probe vocabulary (found/missing/deferred/mismatch). This is deliberately a different axis — and a different word set — from the PERSISTED per-record state in DomainView.dns_records[].status (verified/pending/missing/failed): a probe answers "what did DNS return just now", the persisted status answers "what has the platform recorded about this record". Keep the two vocabularies distinct; see domains_vocab_test.go.
type WebhookDeliveryView ¶
type WebhookDeliveryView struct {
ID string `json:"id"`
EventType string `` /* 271-byte string literal not displayed */
Status string `json:"status" doc:"Delivery state. Open set; tolerate unknown values. Known values: pending, delivered, failed."`
Attempts int `json:"attempts"`
LastError string `json:"last_error,omitempty"`
LastStatusCode *int `json:"last_status_code,omitempty"`
LastAttemptAt *time.Time `json:"last_attempt_at,omitempty" format:"date-time"`
NextRetryAt time.Time `json:"next_retry_at" format:"date-time"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
}
WebhookDeliveryView mirrors the legacy per-delivery wire shape.
type WebhookFiltersRequest ¶
type WebhookFiltersRequest struct {
AgentIDs []string `json:"agent_emails,omitempty" nullable:"false"`
ConversationIDs []string `json:"conversation_ids,omitempty" nullable:"false"`
Labels []string `json:"labels,omitempty" nullable:"false"`
}
WebhookFiltersRequest is WebhookFiltersView's request-side twin (create/ update bodies). Dedicated input type because the spec's forward-compat stance is asymmetric: request schemas stay `additionalProperties: false` (an unknown filter key is a 422, not a silently ignored no-op filter) while response schemas are open so clients tolerate additive fields; one shared schema cannot carry both (stability.go panics if one tries). Keep the fields in lockstep with the View. Convertible via WebhookFiltersView(f) — the compiler enforces the mirror.
type WebhookFiltersView ¶
type WebhookFiltersView struct {
AgentIDs []string `json:"agent_emails,omitempty" nullable:"false"`
ConversationIDs []string `json:"conversation_ids,omitempty" nullable:"false"`
Labels []string `json:"labels,omitempty" nullable:"false"`
}
WebhookFiltersView mirrors identity.WebhookFilters (response side).
type WebhookIDParam ¶
type WebhookIDParam struct {
ID string `path:"id"`
}
WebhookIDParam is the path input for single-webhook read/update ops.
type WebhookView ¶
type WebhookView struct {
ID string `json:"id"`
URL string `json:"url"`
Description string `json:"description"`
Events []string `` /* 628-byte string literal not displayed */
Filters WebhookFiltersView `json:"filters"`
Enabled bool `json:"enabled"`
AutoDisabledAt *time.Time `json:"auto_disabled_at,omitempty" format:"date-time"`
CreatedAt time.Time `json:"created_at" format:"date-time"`
LastDeliveredAt *time.Time `json:"last_delivered_at,omitempty" format:"date-time"`
}
WebhookView is the webhook resource as returned by get/list/update. It carries NO signing secret (WH-3): the secret is shown once, only in the create response (CreateWebhookResponse) and on rotate (rotateSecretResponse).
Source Files
¶
- account.go
- agent_suppressions.go
- agents_write.go
- apikeys.go
- attachments.go
- authentication_schema.go
- contract_headers.go
- conversations.go
- delete_confirm.go
- delete_results.go
- domains.go
- error_catalog.go
- errors.go
- eventpayload_schemas.go
- events.go
- hitl.go
- hold_reason.go
- httpapi.go
- idempotency.go
- messages.go
- middleware.go
- operations.go
- outbound.go
- pagination.go
- protection.go
- ratelimit.go
- review_protection.go
- reviews.go
- scope.go
- stability.go
- startertemplates.go
- templates.go
- unsubscribe.go
- webhooks.go