Documentation
¶
Index ¶
- Constants
- func ValidateWebhookURL(rawURL string) error
- type API
- func (a *API) RegisterRoutes(r *mux.Router)
- func (a *API) RegisterWSRoute(r *mux.Router, handle http.HandlerFunc)
- func (a *API) SetApprovalSigner(s *approvaltoken.Signer)
- func (a *API) SetBillingHookURL(s string)
- func (a *API) SetEnforcer(e limits.Enforcer)
- func (a *API) SetIdempotencyStore(s *idempotency.Store)
- func (a *API) SetInternalAPISecret(s string)
- func (a *API) SetMetrics(m telemetry.Metrics)
- func (a *API) SetNotifier(n *hitlnotify.Notifier)
- func (a *API) SetOAuthProvider(p fosite.OAuth2Provider)
- func (a *API) SetOAuthStorage(s *oauth.Storage)
- func (a *API) SetOutbox(o webhookpub.Outbox)
- func (a *API) SetPoolForEvents(p *pgxpool.Pool)
- func (a *API) SetPublisher(p webhookpub.Publisher)
- func (a *API) SetSubscriberStore(s *webhook.SubscriberStore)
- func (a *API) SetUsageStore(s *usage.Store)
- type AgentInfo
- type ApprovePendingMessageRequest
- type ApprovePendingMessageResponse
- type Attachment
- type AuthHeaders
- type ConversationDetail
- type ConversationSummary
- type CreateSigningSecretRequest
- type CreateSigningSecretResponse
- type CreateWebhookRequest
- type DNSRecord
- type DNSRecords
- type DeleteUserDataResult
- type DeliveryStatus
- type DeploymentInfo
- type DomainInfo
- type ForwardMessageRequest
- type ForwardRequest
- type LimitsCaps
- type LimitsInfo
- type LimitsUsage
- type ListAgentsResponse
- type ListConversationsResponse
- type ListDomainsResponse
- type ListEventsResponse
- type ListMessagesResponse
- type ListPendingMessagesResponse
- type ListSigningSecretsResponse
- type ListWebhookDeliveriesResponse
- type ListWebhooksResponse
- type MessageDetail
- type MessageSummary
- type OAuthClientPublicMetadata
- type OAuthError
- type OAuthMetadata
- type OAuthRegisterRequest
- type OAuthRegisterResponse
- type PendingMessageDetail
- type PendingMessageInboundContext
- type PendingMessageSummary
- type RedeliverDeliveryResult
- type RedeliverRequest
- type RedeliverResponse
- type RedeliverSinceRequest
- type RedeliverSinceResponse
- type RegisterAgentRequest
- type RegisterAgentResponse
- type RegisterDomainRequest
- type RegisterDomainResponse
- type RejectPendingMessageRequest
- type RejectPendingMessageResponse
- type ReplyRequest
- type ReplyToMessageRequest
- type RotateWebhookSecretResponse
- type SendEmailRequest
- type SendEmailResponse
- type SigningSecretSummary
- type TestWebhookRequest
- type TestWebhookResponse
- type UpdateAgentRequest
- type UpdateDomainRequest
- type UpdateMessageRequest
- type UpdateMessageResponse
- type UpdateWebhookRequest
- type UserExport
- type VerifyDomainResponse
- type WebSocketNotification
- type WebhookDeliveryResponse
- type WebhookEvent
- type WebhookFilters
- type WebhookPayload
- type WebhookResponse
Constants ¶
const ( // MaxLabelLength bounds a single label's length to keep the GIN // index entries small and prevent multi-KB tags from being smuggled // into the array. MaxLabelLength = 64 // MaxLabelsPerOp caps the per-request add/remove list size. // Modeled on Gmail's 100/100 cap. Bigger than AgentMail's // (unspecified) but defensive enough that one PATCH can't try // to set thousands of labels. MaxLabelsPerOp = 50 // LabelSystemPrefix marks server-applied system labels. User // writes that try to set a label starting with this prefix are // rejected with 400 — the namespace is reserved so future system // labels (auto-tagged, hitl-approved, …) don't collide with user // tags. LabelSystemPrefix = "e2a:" )
Label validation constants. Public so tests can reference them.
Variables ¶
This section is empty.
Functions ¶
func ValidateWebhookURL ¶
ValidateWebhookImageURL — see ValidateWebhookURL.
ValidateWebhookURL checks that a webhook URL is safe to call (SSRF protection).
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
func (*API) RegisterRoutes ¶
func (*API) RegisterWSRoute ¶
func (a *API) RegisterWSRoute(r *mux.Router, handle http.HandlerFunc)
RegisterWSRoute registers the WebSocket endpoint for local-mode agents.
func (*API) SetApprovalSigner ¶
func (a *API) SetApprovalSigner(s *approvaltoken.Signer)
SetApprovalSigner wires in the magic-link signer after construction so callers (and tests) that don't need HITL magic-link endpoints don't have to know about it. When nil, handleApproveMagicLink / handleRejectMagicLink respond with 404.
func (*API) SetBillingHookURL ¶ added in v0.3.0
SetBillingHookURL wires in the URL of an external billing service's user-event endpoint. When the user deletes their account, the API HMAC-signs a JSON payload and POSTs it there so the billing service can cancel the corresponding Stripe subscription. When empty (the self-host default), no hook fires — appropriate for deployments without a billing service. The same internal_api_secret is reused for the signature.
func (*API) SetEnforcer ¶ added in v0.3.0
SetEnforcer wires in the resource-limits enforcer. When nil (the default) every check passes — handlers behave as if every user has unlimited capacity. The cmd/e2a runtime always sets it; tests that don't care about limits omit it and continue to work as before.
func (*API) SetIdempotencyStore ¶ added in v0.3.0
func (a *API) SetIdempotencyStore(s *idempotency.Store)
SetIdempotencyStore enables Idempotency-Key processing on the outbound /send and /reply endpoints. When nil (the default) the header is silently ignored — keeps the agent package usable in environments that don't have postgres wired or want to disable the feature. The cmd/e2a runtime always sets it.
func (*API) SetInternalAPISecret ¶ added in v0.3.0
SetInternalAPISecret wires in the shared HMAC secret used to authenticate the /api/internal/limits/invalidate endpoint. When empty (default), that endpoint returns 503 — self-host operators who don't run a billing provisioner never need to configure it.
func (*API) SetMetrics ¶ added in v0.3.0
SetMetrics wires the slice 10 observability backend. Default is telemetry.NoOp; production passes telemetry.NewLog() or a real counter backend.
func (*API) SetNotifier ¶
func (a *API) SetNotifier(n *hitlnotify.Notifier)
SetNotifier wires in the HITL notifier. When nil, holdForApproval still persists the pending message but doesn't fire the email — useful for tests that don't want the async SMTP traffic.
func (*API) SetOAuthProvider ¶ added in v0.3.0
func (a *API) SetOAuthProvider(p fosite.OAuth2Provider)
SetOAuthProvider wires in the fosite-backed OAuth provider. When nil, /api/oauth/* endpoints return 404 (matches the SetApprovalSigner / SetNotifier pattern of "optional capability, silently absent when not wired"). Operators who don't want OAuth enabled simply don't call this.
func (*API) SetOAuthStorage ¶ added in v0.3.0
SetOAuthStorage wires in the OAuth storage handle separately from the provider. The consent handler needs Pool() to begin a pgx tx that spans the agent-create (identity pkg) and the auth-code insert (fosite → oauth pkg). Provider-only callers (e.g. /token) don't need it, but it's required for /consent to work; setting one without the other is a misconfiguration the consent handler surfaces as a 503.
func (*API) SetOutbox ¶ added in v0.3.0
func (a *API) SetOutbox(o webhookpub.Outbox)
SetOutbox wires the slice-4 transactional outbox. Used by the outbound /send handler for the email.sent event (post-side-effect: PublishBestEffortTx). Other trigger sites (HITL) continue to use the legacy publisher; they will migrate in a future slice if/when their handlers gain transactional plumbing.
func (*API) SetPoolForEvents ¶ added in v0.3.0
SetPoolForEvents wires the raw pgxpool.Pool used by the events handlers. Kept separate from the store so a future refactor can route through a higher-level abstraction without changing the handler signatures.
func (*API) SetPublisher ¶ added in v0.3.0
func (a *API) SetPublisher(p webhookpub.Publisher)
SetPublisher wires the LEGACY in-process fan-out publisher. Kept during the slice-4→slice-11 rollout window. Safe to leave nil in tests; trigger sites no-op when it is.
func (*API) SetSubscriberStore ¶ added in v0.3.0
func (a *API) SetSubscriberStore(s *webhook.SubscriberStore)
SetSubscriberStore wires the subscriber-store dependency after NewAPI. Same optional-setter convention as SetEnforcer / etc.
func (*API) SetUsageStore ¶ added in v0.3.0
SetUsageStore wires in the usage store used by handleGetMyLimits to surface the user's current counts (agents, domains, messages this month, storage bytes) alongside the resolved caps. Separate from the usage.UsageTracker (which is for recording events) so the dashboard read path can stay alive even when usage-tracking is otherwise off.
type AgentInfo ¶
type AgentInfo struct {
ID string `json:"id" example:"ag_abc123"`
Domain string `json:"domain" example:"agents.example.com"`
Email string `json:"email" example:"my-bot@example.com"`
Name string `json:"name" example:"My Bot"`
WebhookURL string `json:"webhook_url"`
AgentMode string `json:"agent_mode" example:"cloud" enums:"cloud,local"`
DomainVerified bool `json:"domain_verified"`
CreatedAt time.Time `json:"created_at"`
HITLEnabled bool `json:"hitl_enabled"`
HITLTTLSeconds int `json:"hitl_ttl_seconds" example:"604800"`
HITLExpirationAction string `json:"hitl_expiration_action" example:"reject" enums:"approve,reject"`
} // @name Agent
AgentInfo is the public API representation of an agent.
type ApprovePendingMessageRequest ¶
type ApprovePendingMessageRequest struct {
Subject *string `json:"subject,omitempty"`
BodyText *string `json:"body_text,omitempty"`
BodyHTML *string `json:"body_html,omitempty"`
To []string `json:"to,omitempty"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
} // @name ApprovePendingMessageRequest
ApprovePendingMessageRequest is the optional body for POST /api/v1/messages/{id}/approve. Any field present overrides the stored value before the message is sent; missing fields are left as the original draft. An empty body means approve-as-is.
type ApprovePendingMessageResponse ¶
type ApprovePendingMessageResponse struct {
Status string `json:"status" example:"sent"`
MessageID string `json:"message_id"`
ProviderMessageID string `json:"provider_message_id,omitempty"`
Method string `json:"method,omitempty" example:"smtp"`
Edited bool `json:"edited,omitempty"`
} // @name ApprovePendingMessageResponse
ApprovePendingMessageResponse is the response when the reviewer approves a held message and the server successfully hands it to the upstream SMTP relay.
type Attachment ¶
type Attachment = outbound.Attachment
Attachment is a base64-encoded file attachment.
type AuthHeaders ¶
type AuthHeaders struct {
Verified string `json:"X-E2A-Auth-Verified" example:"true"`
Sender string `json:"X-E2A-Auth-Sender" example:"alice@example.com"`
EntityType string `json:"X-E2A-Auth-Entity-Type" example:"human" enums:"human,agent"`
DomainCheck string `json:"X-E2A-Auth-Domain-Check" example:"spf=pass; dkim=none"`
Delegation string `json:"X-E2A-Auth-Delegation,omitempty" example:"agent=ag_abc123;human=usr_xyz789"`
Signature string `json:"X-E2A-Auth-Signature" example:"sha256=..."`
Timestamp string `json:"X-E2A-Auth-Timestamp" example:"2025-01-15T10:30:00Z"`
} // @name AuthHeaders
AuthHeaders documents the signed authentication headers included in webhook deliveries. The server signs Verified, Sender, EntityType, DomainCheck, Delegation, and Timestamp into a canonical string and produces an HMAC-SHA256 Signature. SDKs can verify the signature to confirm the payload was not tampered with.
type ConversationDetail ¶ added in v0.3.0
type ConversationDetail struct {
ConversationSummary
Participants []string `json:"participants"`
Labels []string `json:"labels"`
Messages []MessageSummary `json:"messages"`
} // @name ConversationDetail
ConversationDetail extends the summary with computed aggregates (participants union, label union) and the member messages, ordered chronologically (oldest first).
type ConversationSummary ¶ added in v0.3.0
type ConversationSummary struct {
ConversationID string `json:"conversation_id" example:"conv_abc123"`
LastMessageAt string `json:"last_message_at" example:"2026-05-28T12:00:00Z"`
FirstMessageAt string `json:"first_message_at" example:"2026-05-20T10:00:00Z"`
MessageCount int `json:"message_count" example:"4"`
InboundCount int `json:"inbound_count" example:"2"`
OutboundCount int `json:"outbound_count" example:"2"`
// HasUnread is true iff at least one INBOUND member of this
// conversation is in inbox_status='unread'. Outbound rows do
// NOT contribute — a thread containing only your sent messages
// (or only HITL-pending outbound) returns false. This is the
// agent's mailbox view, not the reviewer's HITL queue.
HasUnread bool `json:"has_unread" example:"true"`
LatestSubject string `json:"latest_subject" example:"Re: Quarterly report"`
LatestSender string `json:"latest_sender" example:"alice@example.com"`
} // @name ConversationSummary
ConversationSummary is one row in the conversations list. Aggregated counts + the "latest" preview fields render an inbox-style conversation list without a per-row drill-down.
type CreateSigningSecretRequest ¶ added in v0.3.0
type CreateSigningSecretRequest struct {
// Optional human-readable label so users can tell secrets apart in
// the dashboard (e.g. "prod", "staging", "rollover-2026-04").
Name string `json:"name"`
}
CreateSigningSecretRequest is the body of POST /api/v1/users/me/signing-secrets.
type CreateSigningSecretResponse ¶ added in v0.3.0
type CreateSigningSecretResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Secret string `json:"secret"`
SecretPrefix string `json:"secret_prefix"`
CreatedAt string `json:"created_at"`
} // @name CreateSigningSecretResponse
CreateSigningSecretResponse is returned exactly once at creation. The plaintext Secret is the only chance the caller has to capture the value — subsequent reads only see SecretPrefix.
type CreateWebhookRequest ¶ added in v0.3.0
type CreateWebhookRequest struct {
URL string `json:"url" example:"https://example.com/e2a/hook"`
Events []string `json:"events" example:"email.received"`
Filters *WebhookFilters `json:"filters,omitempty"`
Description string `json:"description,omitempty" example:"main inbox handler"`
} // @name CreateWebhookRequest
CreateWebhookRequest is the POST /api/v1/webhooks body.
type DNSRecord ¶
type DNSRecord struct {
Host string `json:"host" example:"@"`
Value string `json:"value" example:"mx.example.com"`
Priority *int `json:"priority,omitempty" example:"10"`
} // @name DNSRecord
DNSRecord is a single DNS record entry.
type DNSRecords ¶
type DNSRecords struct {
MX DNSRecord `json:"mx"`
TXT DNSRecord `json:"txt"`
DKIM DNSRecord `json:"dkim,omitempty"`
} // @name DNSRecords
DNSRecords contains the DNS records needed for domain verification. DKIM is populated for domains created after migration 014 (per-domain keypairs). Pre-migration rows leave DKIM at the zero value — clients can detect by checking Host == "".
type DeleteUserDataResult ¶
type DeleteUserDataResult = identity.DeleteUserDataResult // @name DeleteUserDataResult
type DeliveryStatus ¶ added in v0.3.0
type DeliveryStatus struct {
MatchedWebhooks int `json:"matched_webhooks"`
Delivered int `json:"delivered"`
Pending int `json:"pending"`
Failed int `json:"failed"`
} // @name DeliveryStatus
DeliveryStatus summarizes how many of the matched webhooks have received an event so far. Computed at read time by joining against webhook_subscriber_deliveries.
type DeploymentInfo ¶
type DeploymentInfo struct {
// on this deployment (e.g. "agents.example.com"). Empty when the
// operator hasn't configured one — in that case slug registration is
// disabled and every agent must use a custom domain.
SharedDomain string `json:"shared_domain" example:"agents.example.com"`
// SlugRegistrationEnabled mirrors `shared_domain != ""` for clients
// that prefer a boolean. Equivalent to checking SharedDomain directly.
SlugRegistrationEnabled bool `json:"slug_registration_enabled" example:"true"`
// PublicURL is the externally visible base URL of the API itself —
// the same value the operator sets in `http.public_url`. Empty when
// not configured.
PublicURL string `json:"public_url,omitempty" example:"https://e2a.example.com"`
} // @name DeploymentInfo
DeploymentInfo is the response shape of GET /api/v1/info. It's how CLI/SDK clients discover deployment-specific values (shared domain, public URL) without each user having to configure them by hand.
type DomainInfo ¶
type DomainInfo struct {
Domain string `json:"domain" example:"yourdomain.com"`
Verified bool `json:"verified"`
VerificationToken string `json:"verification_token" example:"e2a-verify=abc123"`
DNSRecords DNSRecords `json:"dns_records"`
CreatedAt time.Time `json:"created_at"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
// IsPrimary marks the user's default domain — at most one per
// user, enforced server-side via SetDomainPrimary. The redesign's
// Domains list renders this as a "Primary" chip.
IsPrimary bool `json:"is_primary"`
// LastCheckedAt is the timestamp of the most recent
// /api/v1/domains/{domain}/verify probe (success or failure).
// Distinct from VerifiedAt, which only updates on success.
LastCheckedAt *time.Time `json:"last_checked_at,omitempty"`
// AgentCount is populated by list endpoints. Single-domain endpoints
// (register, verify) leave it zero — the count would require an
// extra query that callers can derive from the agents list anyway.
AgentCount int `json:"agent_count"`
} // @name Domain
DomainInfo is the public API representation of a domain.
type ForwardMessageRequest ¶ added in v0.3.0
type ForwardMessageRequest struct {
To []string `json:"to" example:"alice@example.com"`
CC []string `json:"cc,omitempty" example:"bob@example.com"`
BCC []string `json:"bcc,omitempty" example:"carol@example.com"`
Body string `json:"body,omitempty" example:"FYI — see below"`
HTMLBody string `json:"html_body,omitempty" example:"<p>FYI — see below</p>"`
ConversationID string `json:"conversation_id,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
} // @name ForwardMessageRequest
ForwardMessageRequest is the request body for forwarding a message. Body and html_body are the caller's optional comment to prepend; the server appends a quoted block with the original headers and body. A forward is treated as a new thread (no In-Reply-To/References) — pass conversation_id to bind it to an existing thread explicitly.
type ForwardRequest ¶ added in v0.3.0
type ForwardRequest struct {
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
Body string `json:"body,omitempty"`
HTMLBody string `json:"html_body,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
Attachments []outbound.Attachment `json:"attachments,omitempty"`
}
ForwardRequest is the JSON body for /api/v1/agents/{email}/messages/{id}/forward.
type LimitsCaps ¶ added in v0.3.0
type LimitsInfo ¶ added in v0.3.0
type LimitsInfo struct {
PlanCode string `json:"plan_code"`
Limits LimitsCaps `json:"limits"`
Usage LimitsUsage `json:"usage"`
UpgradeURL string `json:"upgrade_url"`
} // @name LimitsInfo
LimitsInfo is the response body for GET /api/v1/users/me/limits. It bundles the user's resolved caps with their current usage so the dashboard renders the "you're using X of Y" surface in one round trip. plan_code and upgrade_url come straight from the limits row (opaque to OSS — written by whatever provisions the row); when no row exists, plan_code is the operator default and upgrade_url is empty.
type LimitsUsage ¶ added in v0.3.0
type ListAgentsResponse ¶
type ListAgentsResponse struct {
Agents []AgentInfo `json:"agents"`
} // @name ListAgentsResponse
ListAgentsResponse wraps the agent list returned by GET /api/v1/agents.
type ListConversationsResponse ¶ added in v0.3.0
type ListConversationsResponse struct {
Conversations []ConversationSummary `json:"conversations"`
} // @name ListConversationsResponse
ListConversationsResponse wraps the conversations list. Pagination is intentionally deferred — the response is hard-capped at 100 conversations server-side, which covers the inbox-style use case without a cursor.
type ListDomainsResponse ¶
type ListDomainsResponse struct {
Domains []DomainInfo `json:"domains"`
} // @name ListDomainsResponse
ListDomainsResponse wraps the domain list returned by GET /api/v1/domains.
type ListEventsResponse ¶ added in v0.3.0
type ListEventsResponse struct {
Events []WebhookEvent `json:"events"`
NextToken string `json:"next_token,omitempty"`
} // @name ListEventsResponse
ListEventsResponse wraps the events list.
type ListMessagesResponse ¶
type ListMessagesResponse struct {
Messages []MessageSummary `json:"messages"`
NextToken string `json:"next_token,omitempty"`
} // @name ListMessagesResponse
ListMessagesResponse wraps the message list with pagination.
type ListPendingMessagesResponse ¶
type ListPendingMessagesResponse struct {
Messages []PendingMessageSummary `json:"messages"`
} // @name ListPendingMessagesResponse
ListPendingMessagesResponse wraps the array returned by GET /api/v1/messages.
type ListSigningSecretsResponse ¶ added in v0.3.0
type ListSigningSecretsResponse struct {
Secrets []SigningSecretSummary `json:"secrets"`
} // @name ListSigningSecretsResponse
ListSigningSecretsResponse is the GET shape.
type ListWebhookDeliveriesResponse ¶ added in v0.3.0
type ListWebhookDeliveriesResponse struct {
Deliveries []WebhookDeliveryResponse `json:"deliveries"`
} // @name ListWebhookDeliveriesResponse
ListWebhookDeliveriesResponse wraps the deliveries endpoint.
type ListWebhooksResponse ¶ added in v0.3.0
type ListWebhooksResponse struct {
Webhooks []WebhookResponse `json:"webhooks"`
} // @name ListWebhooksResponse
ListWebhooksResponse wraps the list endpoint.
type MessageDetail ¶
type MessageDetail struct {
MessageID string `json:"message_id" example:"msg_abc123"`
From string `json:"from" example:"alice@example.com"`
To []string `json:"to" example:"my-bot@example.com"`
CC []string `json:"cc,omitempty"`
ReplyTo []string `json:"reply_to,omitempty"`
Recipient string `json:"recipient" example:"my-bot@example.com"`
Subject string `json:"subject" example:"Hello"`
ConversationID string `json:"conversation_id,omitempty"`
Status string `json:"status" example:"read"`
// Labels are caller-applied string tags. See MessageSummary.Labels
// for the validation rules. Empty array when no labels are set —
// never null.
Labels []string `json:"labels"`
CreatedAt string `json:"created_at"`
AuthHeaders map[string]string `json:"auth_headers"`
RawMessage string `json:"raw_message"`
} // @name MessageDetail
MessageDetail is the full message content returned by GET /api/v1/agents/{email}/messages/{id}, which marks unread messages as read when fetched. To and CC are the parsed To: / Cc: headers from the original message; Recipient is this delivery's per-agent target. ReplyTo carries the parsed Reply-To: header so consumers can identify the intended reply mailbox for forwarded / notification mail (e.g. From: notifications@..., Reply-To: <real-user>).
type MessageSummary ¶
type MessageSummary struct {
MessageID string `json:"message_id" example:"msg_abc123"`
Direction string `json:"direction" example:"inbound" enums:"inbound,outbound"`
From string `json:"from" example:"alice@example.com"`
To []string `json:"to" example:"my-bot@example.com"`
CC []string `json:"cc,omitempty"`
ReplyTo []string `json:"reply_to,omitempty"`
Recipient string `json:"recipient" example:"my-bot@example.com"`
Subject string `json:"subject" example:"Hello"`
ConversationID string `json:"conversation_id,omitempty"`
// Status carries the inbound inbox_status value (`unread` | `read`).
// Empty string for outbound rows — clients filtering on Status must
// gate on `Direction == "inbound"` first. The enum was removed from
// the swag annotation deliberately so SDK generators don't emit a
// `Literal["unread", "read"]` that breaks at runtime.
Status string `json:"status"`
HITLStatus string `json:"hitl_status,omitempty" example:"sent" enums:"pending_approval,sent,rejected,expired_approved,expired_rejected"`
WebhookStatus string `json:"webhook_status,omitempty" example:"delivered" enums:"pending,delivered,failed"`
WebhookError string `json:"webhook_error,omitempty"`
SizeBytes int `json:"size_bytes,omitempty" example:"4231"`
// Labels are caller-applied string tags. Always lowercase, charset
// `[a-z0-9:_-]+`, ≤ 64 chars each, ≤ 100 per message. The `e2a:`
// prefix is reserved for server-applied system labels. Empty array
// when no labels are set — never null.
Labels []string `json:"labels"`
CreatedAt string `json:"created_at" example:"2025-01-15T10:30:00Z"`
} // @name MessageSummary
MessageSummary is a lightweight message summary for the list endpoint. To and CC are the parsed To: / Cc: headers from the original message; Recipient is this delivery's per-agent target. ReplyTo is the parsed Reply-To: header — empty when the sender did not request a different reply mailbox (the server never falls back to From: silently).
This shape covers both inbound and outbound rows since the dashboard inbox queries `?direction=all`. The non-`omitempty` fields are present on every row; the others are direction-specific:
- Status (inbound): "unread" | "read"; empty string for outbound rows.
- HITLStatus (outbound): the outbound delivery state ("pending_approval", "sent", "rejected", "expired_*"); empty for inbound.
- WebhookStatus (outbound): delivery state of the most recent webhook attempt ("delivered", "pending", "failed"); empty for inbound.
- WebhookError (outbound): last webhook delivery error text when WebhookStatus is "failed"; empty otherwise.
- SizeBytes: raw message length in bytes (best-effort, 0 when the row was migrated from a pre-sizing build).
type OAuthClientPublicMetadata ¶ added in v0.3.0
type OAuthClientPublicMetadata struct {
ClientID string `json:"client_id"`
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
Scopes []string `json:"scopes"`
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
}
OAuthClientPublicMetadata is the subset of an oauth_clients row we surface to anonymous readers. The consent page (web/) fetches this so it can render the friendly client_name beside the requested scope. No secrets are present in this struct; secret_hash and internal bookkeeping fields are deliberately omitted.
type OAuthError ¶ added in v0.3.0
type OAuthError struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description,omitempty"`
}
OAuthError is the RFC 7591 §3.2.2 / RFC 6749 §5.2 JSON error body. Used for DCR-side errors and direct (non-redirected) authorize errors.
type OAuthMetadata ¶ added in v0.3.0
type OAuthMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
ResponseTypesSupported []string `json:"response_types_supported"`
ResponseModesSupported []string `json:"response_modes_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported"`
ScopesSupported []string `json:"scopes_supported"`
// RFC 9207 §3 — advertises that authorize responses carry `iss`
// (we emit it manually in writeAuthorizeRedirect since fosite
// v0.49 doesn't ship native RFC 9207 support).
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
}
OAuthMetadata is the RFC 8414 authorization-server metadata document served at /.well-known/oauth-authorization-server. Field names use snake_case per §2 of the RFC. Only the fields e2a actually advertises are present — omitting an OPTIONAL field (introspection_endpoint, jwks_uri, …) signals to clients that the feature isn't supported.
type OAuthRegisterRequest ¶ added in v0.3.0
type OAuthRegisterRequest struct {
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
Scope string `json:"scope,omitempty"`
}
OAuthRegisterRequest is the RFC 7591 §2 client metadata POSTed to /api/oauth/register. Unknown fields are tolerated (forward-compat with RFC 7591 extensions) but ignored.
type OAuthRegisterResponse ¶ added in v0.3.0
type OAuthRegisterResponse struct {
ClientID string `json:"client_id"`
ClientIDIssuedAt int64 `json:"client_id_issued_at"`
ClientName string `json:"client_name"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
Scope string `json:"scope"`
}
OAuthRegisterResponse is the RFC 7591 §3.2.1 success envelope. Echoes the metadata the server stored (after defaults applied) plus the assigned client_id and issuance timestamp. No client_secret — public clients only.
type PendingMessageDetail ¶
type PendingMessageDetail struct {
PendingMessageSummary
EmailMessageID string `json:"email_message_id,omitempty" example:"<orig@gmail.com>"`
BodyText string `json:"body_text,omitempty"`
BodyHTML string `json:"body_html,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Edited bool `json:"edited,omitempty"`
// InboundContext is attached when this is a reply — provides the
// SPF/DKIM/DMARC provenance + sender/subject of the inbound message
// being replied to so the review panel can render the context pane.
InboundContext *PendingMessageInboundContext `json:"inbound,omitempty"`
ReviewedAt string `json:"reviewed_at,omitempty" example:"2025-01-15T10:35:00Z"`
// ReviewedByUserID identifies the human reviewer for approved or
// rejected messages. NULL on TTL-expired transitions (worker
// auto-approve / auto-reject) where no human reviewed the message.
ReviewedByUserID *string `json:"reviewed_by_user_id,omitempty" example:"usr_abc123"`
// ReviewedByName is the JOIN'd display name from the reviewer's
// users row. NULL when reviewed_by_user_id is null (worker) or when
// the reviewer's user account has since been deleted (the FK has
// ON DELETE SET NULL specifically so this doesn't poison the audit
// trail).
ReviewedByName *string `json:"reviewed_by_name,omitempty" example:"Jamie"`
RejectionReason string `json:"rejection_reason,omitempty"`
ProviderMessageID string `json:"provider_message_id,omitempty"`
Method string `json:"method,omitempty" example:"smtp"`
} // @name PendingMessageDetail
PendingMessageDetail extends the summary with the stored body, attachments, and review metadata. Body columns are populated only while the row is in pending_approval; terminal rows return empty bodies since the server scrubs them on transition.
type PendingMessageInboundContext ¶ added in v0.3.0
type PendingMessageInboundContext struct {
Sender string `json:"sender" example:"alice@gmail.com"`
Subject string `json:"subject" example:"contract details"`
CreatedAt string `json:"created_at" example:"2025-01-15T10:25:00Z"`
// AuthHeaders carries the SPF/DKIM/DMARC validation results captured
// at inbound time. Keys are conventionally "spf", "dkim", "dmarc"
// each with values "pass" | "fail" | "neutral" | etc. The dashboard
// renders these as found/missing chips on the provenance pane.
AuthHeaders map[string]string `json:"auth_headers,omitempty"`
} // @name PendingMessageInboundContext
PendingMessageInboundContext is the inlined inbound-row preview attached to a reply's pending detail. Body is intentionally elided (the inbound's raw_message is RFC 5322 bytes; the review panel surfaces only the headers + auth_headers).
type PendingMessageSummary ¶
type PendingMessageSummary struct {
ID string `json:"id" example:"msg_abc123"`
AgentID string `json:"agent_id" example:"my-bot@example.com"`
Direction string `json:"direction" example:"outbound"`
Subject string `json:"subject" example:"Re: contract details"`
Type string `json:"type,omitempty" example:"send" enums:"send,reply,test,forward"`
ConversationID string `json:"conversation_id,omitempty"`
To []string `json:"to" example:"alice@example.com"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
Status string `json:"status" example:"pending_approval" enums:"sent,pending_approval,rejected,expired_approved,expired_rejected"`
ApprovalExpiresAt string `json:"approval_expires_at,omitempty" example:"2025-01-15T10:30:00Z"`
CreatedAt time.Time `json:"created_at"`
} // @name PendingMessageSummary
PendingMessageSummary is a row in GET /api/v1/messages?status=pending_approval. Body and attachments are intentionally omitted — fetch the full detail via GET /api/v1/messages/{id} when the reviewer drills in.
type RedeliverDeliveryResult ¶ added in v0.3.0
type RedeliverDeliveryResult struct {
WebhookID string `json:"webhook_id"`
DeliveryID string `json:"delivery_id,omitempty"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
} // @name RedeliverDeliveryResult
RedeliverDeliveryResult is one element of a fan-out replay response.
type RedeliverRequest ¶ added in v0.3.0
type RedeliverRequest struct {
WebhookID string `json:"webhook_id,omitempty"`
} // @name RedeliverRequest
RedeliverRequest is the body of POST /events/{id}/redeliver.
type RedeliverResponse ¶ added in v0.3.0
type RedeliverResponse struct {
DeliveryID string `json:"delivery_id,omitempty"`
EventID string `json:"event_id"`
WebhookID string `json:"webhook_id,omitempty"`
Status string `json:"status"`
Deliveries []RedeliverDeliveryResult `json:"deliveries,omitempty"`
} // @name RedeliverResponse
RedeliverResponse wraps the result of a replay request.
type RedeliverSinceRequest ¶ added in v0.3.0
type RedeliverSinceRequest struct {
Since string `json:"since"`
} // @name RedeliverSinceRequest
RedeliverSinceRequest is the body of POST /webhooks/{id}/redeliver-since.
type RedeliverSinceResponse ¶ added in v0.3.0
type RedeliverSinceResponse struct {
WebhookID string `json:"webhook_id"`
Since string `json:"since"`
Scheduled int `json:"scheduled"`
SkippedAlreadyPending int `json:"skipped_already_pending"`
} // @name RedeliverSinceResponse
RedeliverSinceResponse wraps the result of a bulk-replay request.
type RegisterAgentRequest ¶
type RegisterAgentRequest struct {
Email string `json:"email" example:"my-bot@yourdomain.com"`
Slug string `json:"slug" example:"my-bot"`
Name string `json:"name" example:"My Bot"`
WebhookURL string `json:"webhook_url,omitempty" example:"https://example.com/e2a/webhook"`
// AgentMode selects how inbound mail is delivered. Required; must be "local" or "cloud". See the type-level docs for the difference.
AgentMode string `json:"agent_mode" example:"local" enums:"local,cloud" binding:"required"`
} // @name RegisterAgentRequest
RegisterAgentRequest is the request body for POST /api/v1/agents.
`agent_mode` is required and must be either "local" or "cloud":
"local" — the e2a server queues inbound mail in its own store and pushes notifications over a WebSocket (`/api/v1/agents/{email}/ws`) or makes it pollable via the REST API. Use this when the agent runs on a laptop, edge device, or behind NAT without a public URL. `webhook_url` is not required.
"cloud" — the e2a server delivers inbound mail to the agent over HTTPS POST to `webhook_url`. Use this when the agent is deployed somewhere publicly reachable. `webhook_url` MUST be set and must point to an HTTPS endpoint that resolves to a non-private IP.
type RegisterAgentResponse ¶
type RegisterDomainRequest ¶
type RegisterDomainRequest struct {
Domain string `json:"domain" example:"yourdomain.com"`
} // @name RegisterDomainRequest
RegisterDomainRequest is the request body for POST /api/v1/domains.
type RegisterDomainResponse ¶
type RegisterDomainResponse = DomainInfo // @name RegisterDomainResponse
RegisterDomainResponse is the response for POST /api/v1/domains.
type RejectPendingMessageRequest ¶
type RejectPendingMessageRequest struct {
Reason string `json:"reason,omitempty" example:"wrong recipient"`
} // @name RejectPendingMessageRequest
RejectPendingMessageRequest is the optional body for POST /api/v1/messages/{id}/reject.
type RejectPendingMessageResponse ¶
type RejectPendingMessageResponse struct {
Status string `json:"status" example:"rejected"`
MessageID string `json:"message_id"`
RejectionReason string `json:"rejection_reason,omitempty"`
} // @name RejectPendingMessageResponse
RejectPendingMessageResponse is the response when the reviewer rejects a held message.
type ReplyRequest ¶
type ReplyRequest struct {
Body string `json:"body"`
HTMLBody string `json:"html_body,omitempty"`
ReplyAll bool `json:"reply_all,omitempty"`
CC []string `json:"cc,omitempty"`
BCC []string `json:"bcc,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
Attachments []outbound.Attachment `json:"attachments,omitempty"`
}
type ReplyToMessageRequest ¶
type ReplyToMessageRequest struct {
Body string `json:"body" example:"Thanks for your email!"`
HTMLBody string `json:"html_body,omitempty" example:"<p>Thanks for your email!</p>"`
ReplyAll bool `json:"reply_all,omitempty" example:"false"`
CC []string `json:"cc,omitempty" example:"bob@example.com"`
BCC []string `json:"bcc,omitempty" example:"carol@example.com"`
ConversationID string `json:"conversation_id,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
} // @name ReplyToMessageRequest
ReplyToMessageRequest is the request body for replying to a message.
type RotateWebhookSecretResponse ¶ added in v0.3.0
type RotateWebhookSecretResponse struct {
SigningSecret string `json:"signing_secret"`
PreviousSecretExpiresAt string `json:"previous_secret_expires_at"`
} // @name RotateWebhookSecretResponse
RotateWebhookSecretResponse carries the new plaintext secret + the 24h expiry for the previous secret's grace window.
type SendEmailRequest ¶
type SendEmailRequest struct {
From string `json:"from,omitempty" example:"my-bot@example.com"`
To []string `json:"to" example:"alice@example.com"`
CC []string `json:"cc,omitempty" example:"bob@example.com"`
BCC []string `json:"bcc,omitempty" example:"carol@example.com"`
Subject string `json:"subject" example:"Hello from my agent"`
Body string `json:"body" example:"Hi Alice, this is my agent reaching out."`
HTMLBody string `json:"html_body,omitempty" example:"<p>Hi Alice</p>"`
ConversationID string `json:"conversation_id,omitempty" example:"conv_abc123"`
Attachments []Attachment `json:"attachments,omitempty"`
} // @name SendEmailRequest
SendEmailRequest is the request body for POST /api/v1/send.
type SendEmailResponse ¶
type SendEmailResponse struct {
Status string `json:"status" example:"sent" enums:"sent,pending_approval"`
MessageID string `json:"message_id" example:"msg_abc123"`
Method string `json:"method,omitempty" example:"smtp"`
ApprovalExpiresAt string `json:"approval_expires_at,omitempty" example:"2025-01-15T10:30:00Z"`
} // @name SendEmailResponse
SendEmailResponse is the response for send and reply operations. When the owning agent has HITL enabled, the server responds with status = "pending_approval" and 202 Accepted; approval_expires_at is set in that case. Otherwise status = "sent" with 200.
type SigningSecretSummary ¶ added in v0.3.0
type SigningSecretSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Secret string `json:"secret"`
SecretPrefix string `json:"secret_prefix"`
CreatedAt string `json:"created_at"`
LastSignedAt *string `json:"last_signed_at,omitempty"`
} // @name SigningSecretSummary
SigningSecretSummary is the list shape. Includes the full plaintext `secret` because the relay already stores it that way (it has to — it signs with it on every inbound). Surfacing it on list lets the dashboard show the value on demand without storing yet another copy. API keys are deliberately not handled the same way: their hashes-only posture is intentional and we keep it. The `secret_prefix` field stays populated for compact list views.
type TestWebhookRequest ¶ added in v0.3.0
type TestWebhookRequest struct {
Event string `json:"event" example:"email.received"`
Data map[string]interface{} `json:"data,omitempty"`
} // @name TestWebhookRequest
TestWebhookRequest fires a synthetic event for development.
type TestWebhookResponse ¶ added in v0.3.0
type TestWebhookResponse struct {
DeliveryID string `json:"delivery_id"`
} // @name TestWebhookResponse
TestWebhookResponse echoes the delivery id of the synthetic event so the caller can correlate it in GET /webhooks/{id}/deliveries.
type UpdateAgentRequest ¶
type UpdateAgentRequest struct {
WebhookURL *string `json:"webhook_url,omitempty"`
AgentMode *string `json:"agent_mode,omitempty" enums:"cloud,local"`
HITLEnabled *bool `json:"hitl_enabled,omitempty"`
HITLTTLSeconds *int `json:"hitl_ttl_seconds,omitempty" example:"604800"`
HITLExpirationAction *string `json:"hitl_expiration_action,omitempty" enums:"approve,reject"`
} // @name UpdateAgentRequest
UpdateAgentRequest is the request body for PUT /api/v1/agents/{email}. All fields are optional; only the fields you send are updated, so callers can PATCH a single setting without re-sending the rest.
type UpdateDomainRequest ¶ added in v0.3.0
type UpdateDomainRequest struct {
IsPrimary *bool `json:"is_primary,omitempty"`
} // @name UpdateDomainRequest
UpdateDomainRequest is the body for PATCH /api/v1/domains/{domain}. Only `is_primary=true` is meaningful — see handleUpdateDomain.
type UpdateMessageRequest ¶ added in v0.3.0
type UpdateMessageRequest struct {
AddLabels []string `json:"add_labels,omitempty" example:"urgent"`
RemoveLabels []string `json:"remove_labels,omitempty" example:"unread"`
} // @name UpdateMessageRequest
UpdateMessageRequest is the request body for PATCH /api/v1/agents/{email}/messages/{id}. Currently the only supported mutation is the labels delta — passing an empty body is a no-op. Both add_labels and remove_labels may be set in one request; on overlap the remove wins (the union is applied first, then the difference, mirroring Gmail's semantics).
type UpdateMessageResponse ¶ added in v0.3.0
type UpdateMessageResponse struct {
MessageID string `json:"message_id" example:"msg_abc123"`
Labels []string `json:"labels"`
} // @name UpdateMessageResponse
UpdateMessageResponse is the response shape for label mutations. Returns the post-update label set so callers can echo state without a separate fetch.
type UpdateWebhookRequest ¶ added in v0.3.0
type UpdateWebhookRequest struct {
URL *string `json:"url,omitempty"`
Events *[]string `json:"events,omitempty"`
Filters *WebhookFilters `json:"filters,omitempty"`
Description *string `json:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
} // @name UpdateWebhookRequest
UpdateWebhookRequest is the PATCH body. All fields optional; only fields present in the request are touched. url / events / filters are full-replace when present (the sent value is canonical).
type UserExport ¶
type UserExport = identity.UserExport // @name UserExport
type VerifyDomainResponse ¶
type VerifyDomainResponse struct {
Domain string `json:"domain" example:"yourdomain.com"`
Verified bool `json:"verified"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
// MX status: "found" iff at least one MX record points at the
// deployment's smtp domain. "missing" otherwise.
MX string `json:"mx,omitempty" example:"found" enums:"found,missing"`
// SPF status: "found" iff a v=spf1 TXT record includes the
// deployment's send domain. "missing" otherwise.
SPF string `json:"spf,omitempty" example:"found" enums:"found,missing"`
// DKIM status: "found" iff the published TXT record at
// "{selector}._domainkey.{domain}" matches the per-domain public
// key stored at registration time. "missing" iff a keypair is
// stored but the TXT record isn't published yet. "deferred" iff
// no keypair is stored — pre-migration rows that haven't been
// re-claimed since #5 shipped. A fresh-claimed domain always has
// a keypair, so "deferred" only appears on legacy data.
DKIM string `json:"dkim,omitempty" example:"found" enums:"found,missing,deferred"`
} // @name VerifyDomainResponse
VerifyDomainResponse is the response for POST /api/v1/domains/{domain}/verify. Per-record diagnostic fields (MX, SPF, DKIM) report what the probe found in DNS independent of the verified bool — verified=true iff the TXT ownership token is present, while MX/SPF/DKIM are advisory.
type WebSocketNotification ¶
type WebSocketNotification struct {
MessageID string `json:"message_id" example:"msg_abc123"`
ConversationID string `json:"conversation_id,omitempty"`
From string `json:"from" example:"alice@example.com"`
Recipient string `json:"recipient" example:"my-bot@example.com"`
Subject string `json:"subject" example:"Hello"`
ReceivedAt time.Time `json:"received_at"`
} // @name WebSocketNotification
WebSocketNotification is the lightweight notification sent over WebSocket when a new message arrives for an agent. It contains only metadata — the full message content (including the To/Cc lists) is fetched via GET /api/v1/agents/{email}/messages/{id}.
type WebhookDeliveryResponse ¶ added in v0.3.0
type WebhookDeliveryResponse struct {
ID string `json:"id"`
EventType string `json:"event_type"`
Status string `json:"status"`
Attempts int `json:"attempts"`
LastError string `json:"last_error,omitempty"`
LastStatusCode *int `json:"last_status_code,omitempty"`
LastAttemptAt string `json:"last_attempt_at,omitempty"`
NextRetryAt string `json:"next_retry_at"`
CreatedAt string `json:"created_at"`
} // @name WebhookDeliveryResponse
WebhookDeliveryResponse is one row in GET /webhooks/{id}/deliveries.
type WebhookEvent ¶ added in v0.3.0
type WebhookEvent struct {
ID string `json:"id"`
Type string `json:"type"`
SchemaVersion int `json:"schema_version"`
CreatedAt string `json:"created_at"`
AgentID *string `json:"agent_id,omitempty"`
ConversationID *string `json:"conversation_id,omitempty"`
MessageID *string `json:"message_id,omitempty"`
Status string `json:"status"`
Data map[string]interface{} `json:"data"`
DeliveryStatus *DeliveryStatus `json:"delivery_status,omitempty"`
} // @name WebhookEvent
WebhookEvent is the wire shape returned by GET /events and GET /events/{id}. Mirrors design §4.6.
type WebhookFilters ¶ added in v0.3.0
type WebhookFilters struct {
AgentIDs []string `json:"agent_ids,omitempty"`
ConversationIDs []string `json:"conversation_ids,omitempty"`
Labels []string `json:"labels,omitempty"`
} // @name WebhookFilters
WebhookFilters is the structured scope filter on a webhook. Empty / missing keys mean "no constraint of that type". A webhook with all-empty filters is a cross-cutting subscriber that matches every event of the right type for the owning user.
type WebhookPayload ¶
type WebhookPayload struct {
MessageID string `json:"message_id" example:"msg_abc123"`
ConversationID string `json:"conversation_id,omitempty"`
From string `json:"from" example:"alice@example.com"`
To []string `json:"to" example:"agent@yourdomain.com"`
CC []string `json:"cc,omitempty"`
Recipient string `json:"recipient" example:"agent@yourdomain.com"`
RawMessage []byte `json:"raw_message"`
AuthHeaders map[string]string `json:"auth_headers"`
ReceivedAt time.Time `json:"received_at"`
} // @name WebhookPayload
WebhookPayload is the payload delivered to your webhook URL when your agent receives an email. This schema is for documentation only — the actual delivery is handled by the webhook package. To and CC are the parsed To: / Cc: headers; Recipient is the per-agent target for this delivery.
type WebhookResponse ¶ added in v0.3.0
type WebhookResponse struct {
ID string `json:"id" example:"wh_abc123"`
URL string `json:"url"`
Description string `json:"description"`
Events []string `json:"events"`
Filters WebhookFilters `json:"filters"`
SigningSecret string `json:"signing_secret,omitempty"` // ONLY on create + rotate
PreviousSecretExpiresAt string `json:"previous_secret_expires_at,omitempty"` // ONLY on rotate
Enabled bool `json:"enabled"`
AutoDisabledAt string `json:"auto_disabled_at,omitempty"`
CreatedAt string `json:"created_at"`
LastDeliveredAt string `json:"last_delivered_at,omitempty"`
} // @name WebhookResponse
WebhookResponse is the GET / POST response shape. SigningSecret is populated only on POST /webhooks and POST /webhooks/{id}/rotate-secret — every other endpoint omits it so a stolen API key cannot exfiltrate secrets via list/get.