package models import ( "encoding/json" "strings" "time" "github.com/google/uuid" ) // WebhookEventType is the canonical event-name identifier carried both in // the subscription filter and the delivery payload. Keep these stable — // renaming a value breaks every customer subscribed to it. type WebhookEventType string const ( // Email account lifecycle WebhookEventEmailAccountConnected WebhookEventType = "email_account.connected" WebhookEventEmailAccountRemoved WebhookEventType = "email_account.removed" // Campaign send pipeline WebhookEventCampaignEmailSent WebhookEventType = "campaign.email_sent" WebhookEventCampaignEmailDelivered WebhookEventType = "campaign.email_delivered" WebhookEventCampaignEmailOpened WebhookEventType = "campaign.email_opened" WebhookEventCampaignEmailClicked WebhookEventType = "campaign.email_clicked" WebhookEventCampaignEmailBounced WebhookEventType = "campaign.email_bounced" WebhookEventCampaignReplyReceived WebhookEventType = "campaign.reply_received" WebhookEventCampaignUnsubscribed WebhookEventType = "campaign.unsubscribed" WebhookEventCampaignStarted WebhookEventType = "campaign.started" WebhookEventCampaignPaused WebhookEventType = "campaign.paused" WebhookEventCampaignCompleted WebhookEventType = "campaign.completed" // campaign.deliverability_warning fires when a campaign's rolling // bounce/complaint rate enters the early-warning band (half the pause // threshold) — a graduated signal short of an auto-pause. WebhookEventCampaignDeliverabilityWarning WebhookEventType = "campaign.deliverability_warning" // campaign.action fires from a "notify" action node in a sequence flow. WebhookEventCampaignAction WebhookEventType = "campaign.action" // Warmup WebhookEventWarmupEmailSent WebhookEventType = "warmup.email_sent" WebhookEventWarmupHealthChanged WebhookEventType = "warmup.health_changed" WebhookEventWarmupPlacementInSpam WebhookEventType = "warmup.placement_in_spam" WebhookEventWarmupQuarantined WebhookEventType = "warmup.quarantined" WebhookEventWarmupBlocked WebhookEventType = "warmup.blocked" // Deliverability WebhookEventDeliverabilityBounce WebhookEventType = "deliverability.bounce" WebhookEventDeliverabilityComplaint WebhookEventType = "deliverability.complaint" // Meetings (inbound from Calendly / Cal.com). A booked call is one of the // most actionable signals an outbound team gets, so it is a first-class // event other connections can subscribe to (Slack ping, CRM upsert). WebhookEventMeetingBooked WebhookEventType = "meeting.booked" WebhookEventMeetingRescheduled WebhookEventType = "meeting.rescheduled" WebhookEventMeetingCanceled WebhookEventType = "meeting.canceled" // Inbound webhook: a trigger only (never emitted outbound). An external // system POSTs JSON to a per-automation URL, running that one automation with // the body as the event payload. Routed by its URL token, not org fan-out. WebhookEventInboundWebhook WebhookEventType = "inbound.webhook" // --- Inbox / mailbox mail (high-volume; opt-in, see firehoseEvents) --- WebhookEventInboxEmailReceived WebhookEventType = "inbox.email_received" WebhookEventInboxEmailUpdated WebhookEventType = "inbox.email_updated" WebhookEventInboxEmailDeleted WebhookEventType = "inbox.email_deleted" WebhookEventInboxReplyReceived WebhookEventType = "inbox.reply_received" // --- Email account (mailbox) lifecycle & health --- WebhookEventEmailAccountDisconnected WebhookEventType = "email_account.disconnected" WebhookEventEmailAccountError WebhookEventType = "email_account.error" WebhookEventEmailAccountSynced WebhookEventType = "email_account.synced" WebhookEventEmailAccountHealthChanged WebhookEventType = "email_account.health_changed" // --- Contacts --- WebhookEventContactCreated WebhookEventType = "contact.created" WebhookEventContactUpdated WebhookEventType = "contact.updated" WebhookEventContactDeleted WebhookEventType = "contact.deleted" // Forms. WebhookEventFormSubmitted WebhookEventType = "form.submitted" // --- Bulk import/export operations --- WebhookEventBulkOperationStarted WebhookEventType = "bulk_operation.started" WebhookEventBulkOperationCompleted WebhookEventType = "bulk_operation.completed" WebhookEventBulkOperationFailed WebhookEventType = "bulk_operation.failed" // --- Automations --- WebhookEventAutomationCreated WebhookEventType = "automation.created" WebhookEventAutomationUpdated WebhookEventType = "automation.updated" WebhookEventAutomationDeleted WebhookEventType = "automation.deleted" WebhookEventAutomationRun WebhookEventType = "automation.run" // --- Campaign lifecycle / config (the audit-bridge twins of campaign.*) --- WebhookEventCampaignCreated WebhookEventType = "campaign.created" WebhookEventCampaignUpdated WebhookEventType = "campaign.updated" WebhookEventCampaignDeleted WebhookEventType = "campaign.deleted" // --- Templates --- WebhookEventTemplateCreated WebhookEventType = "template.created" WebhookEventTemplateUpdated WebhookEventType = "template.updated" WebhookEventTemplateDeleted WebhookEventType = "template.deleted" // --- Team & access governance --- WebhookEventTeamMemberInvited WebhookEventType = "team.member_invited" WebhookEventTeamMemberRemoved WebhookEventType = "team.member_removed" WebhookEventRoleCreated WebhookEventType = "role.created" WebhookEventRoleUpdated WebhookEventType = "role.updated" WebhookEventRoleDeleted WebhookEventType = "role.deleted" // --- CRM --- WebhookEventCRMDealCreated WebhookEventType = "crm.deal_created" WebhookEventCRMDealUpdated WebhookEventType = "crm.deal_updated" WebhookEventCRMDealDeleted WebhookEventType = "crm.deal_deleted" WebhookEventCRMTaskCreated WebhookEventType = "crm.task_created" WebhookEventCRMTaskUpdated WebhookEventType = "crm.task_updated" WebhookEventCRMNoteCreated WebhookEventType = "crm.note_created" WebhookEventCRMPipelineChange WebhookEventType = "crm.pipeline_updated" // --- Lead sync / settings / billing --- WebhookEventLeadSyncSourceUpdated WebhookEventType = "lead_sync_source.updated" WebhookEventSettingsUpdated WebhookEventType = "settings.updated" WebhookEventSubscriptionUpdated WebhookEventType = "subscription.updated" // --- Developer fire-event (mirrors the gateway CUSTOM_EVENT to HTTP) --- WebhookEventCustom WebhookEventType = "custom.event" // --- System: endpoint verification ping (delivered only to the endpoint // being verified / tested, never fanned out org-wide) --- WebhookEventEndpointTest WebhookEventType = "webhook.test" ) // AllWebhookEventTypes lists every emitted event so the CRUD endpoint can // validate `event_types` filters and the UI can render a picker. var AllWebhookEventTypes = []WebhookEventType{ WebhookEventEmailAccountConnected, WebhookEventEmailAccountRemoved, WebhookEventCampaignEmailSent, WebhookEventCampaignEmailDelivered, WebhookEventCampaignEmailOpened, WebhookEventCampaignEmailClicked, WebhookEventCampaignEmailBounced, WebhookEventCampaignReplyReceived, WebhookEventCampaignUnsubscribed, WebhookEventCampaignStarted, WebhookEventCampaignPaused, WebhookEventCampaignCompleted, WebhookEventCampaignDeliverabilityWarning, WebhookEventCampaignAction, WebhookEventWarmupEmailSent, WebhookEventWarmupHealthChanged, WebhookEventWarmupPlacementInSpam, WebhookEventWarmupQuarantined, WebhookEventWarmupBlocked, WebhookEventDeliverabilityBounce, WebhookEventDeliverabilityComplaint, WebhookEventMeetingBooked, WebhookEventMeetingRescheduled, WebhookEventMeetingCanceled, WebhookEventInboundWebhook, WebhookEventInboxEmailReceived, WebhookEventInboxEmailUpdated, WebhookEventInboxEmailDeleted, WebhookEventInboxReplyReceived, WebhookEventEmailAccountDisconnected, WebhookEventEmailAccountError, WebhookEventEmailAccountSynced, WebhookEventEmailAccountHealthChanged, WebhookEventContactCreated, WebhookEventContactUpdated, WebhookEventContactDeleted, WebhookEventFormSubmitted, WebhookEventBulkOperationStarted, WebhookEventBulkOperationCompleted, WebhookEventBulkOperationFailed, WebhookEventAutomationCreated, WebhookEventAutomationUpdated, WebhookEventAutomationDeleted, WebhookEventAutomationRun, WebhookEventCampaignCreated, WebhookEventCampaignUpdated, WebhookEventCampaignDeleted, WebhookEventTemplateCreated, WebhookEventTemplateUpdated, WebhookEventTemplateDeleted, WebhookEventTeamMemberInvited, WebhookEventTeamMemberRemoved, WebhookEventRoleCreated, WebhookEventRoleUpdated, WebhookEventRoleDeleted, WebhookEventCRMDealCreated, WebhookEventCRMDealUpdated, WebhookEventCRMDealDeleted, WebhookEventCRMTaskCreated, WebhookEventCRMTaskUpdated, WebhookEventCRMNoteCreated, WebhookEventCRMPipelineChange, WebhookEventLeadSyncSourceUpdated, WebhookEventSettingsUpdated, WebhookEventSubscriptionUpdated, WebhookEventCustom, } func IsValidWebhookEventType(s string) bool { for _, t := range AllWebhookEventTypes { if string(t) == s { return true } } return false } // firehoseEvents are per-message, high-volume event types. They are NOT included // in the empty-filter wildcard: an endpoint receives them only by listing them // explicitly in event_types. This keeps a "subscribe to everything" endpoint // from being buried under per-open/click/send traffic, while still letting a // caller opt in deliberately. Keep in sync with the dispatch/throttle sizing. var firehoseEvents = map[WebhookEventType]bool{ WebhookEventCampaignEmailSent: true, WebhookEventCampaignEmailDelivered: true, WebhookEventCampaignEmailOpened: true, WebhookEventCampaignEmailClicked: true, WebhookEventWarmupEmailSent: true, WebhookEventInboxEmailReceived: true, WebhookEventInboxEmailUpdated: true, WebhookEventInboxEmailDeleted: true, WebhookEventEmailAccountSynced: true, } // IsFirehoseEvent reports whether an event is high-volume and therefore // opt-in-only (excluded from the empty-filter "all events" subscription). func IsFirehoseEvent(t WebhookEventType) bool { return firehoseEvents[t] } // WebhookEventCategory groups events for the picker UI and the public catalog. type WebhookEventCategory string const ( WebhookCatCampaign WebhookEventCategory = "Campaign" WebhookCatWarmup WebhookEventCategory = "Warmup" WebhookCatDeliverability WebhookEventCategory = "Deliverability" WebhookCatInbox WebhookEventCategory = "Inbox" WebhookCatEmailAccount WebhookEventCategory = "Mailbox" WebhookCatContact WebhookEventCategory = "Contact" WebhookCatCRM WebhookEventCategory = "CRM" WebhookCatAutomation WebhookEventCategory = "Automation" WebhookCatMeeting WebhookEventCategory = "Meeting" WebhookCatTeam WebhookEventCategory = "Team & access" WebhookCatBulk WebhookEventCategory = "Bulk operations" WebhookCatWorkspace WebhookEventCategory = "Workspace" WebhookCatDeveloper WebhookEventCategory = "Developer" ) // WebhookEventDescriptor is one entry in the public event catalog. The dashboard // renders these in the event picker; the catalog endpoint serves them so // integrators can discover the full vocabulary without scraping our docs. type WebhookEventDescriptor struct { Type WebhookEventType `json:"type"` Category WebhookEventCategory `json:"category"` Description string `json:"description"` Firehose bool `json:"firehose"` } // WebhookEventCatalog is the ordered, described list of every outbound event. // inbound.webhook is intentionally excluded — it is a trigger, never delivered. var WebhookEventCatalog = buildWebhookEventCatalog() func buildWebhookEventCatalog() []WebhookEventDescriptor { type row struct { t WebhookEventType cat WebhookEventCategory desc string } rows := []row{ {WebhookEventCampaignStarted, WebhookCatCampaign, "A campaign began sending."}, {WebhookEventCampaignPaused, WebhookCatCampaign, "A campaign was paused (manually or by deliverability guardrails)."}, {WebhookEventCampaignCompleted, WebhookCatCampaign, "A campaign finished its last scheduled send."}, {WebhookEventCampaignCreated, WebhookCatCampaign, "A campaign was created."}, {WebhookEventCampaignUpdated, WebhookCatCampaign, "A campaign's settings or steps changed."}, {WebhookEventCampaignDeleted, WebhookCatCampaign, "A campaign was deleted."}, {WebhookEventCampaignEmailSent, WebhookCatCampaign, "A campaign email was sent to a contact."}, {WebhookEventCampaignEmailDelivered, WebhookCatCampaign, "A campaign email was accepted by the receiving server."}, {WebhookEventCampaignEmailOpened, WebhookCatCampaign, "A campaign email was opened."}, {WebhookEventCampaignEmailClicked, WebhookCatCampaign, "A link in a campaign email was clicked."}, {WebhookEventCampaignEmailBounced, WebhookCatCampaign, "A campaign email bounced."}, {WebhookEventCampaignReplyReceived, WebhookCatCampaign, "A contact replied to a campaign email."}, {WebhookEventCampaignUnsubscribed, WebhookCatCampaign, "A contact unsubscribed from a campaign."}, {WebhookEventCampaignDeliverabilityWarning, WebhookCatCampaign, "A campaign entered the deliverability early-warning band."}, {WebhookEventCampaignAction, WebhookCatCampaign, "A campaign sequence \"notify\" action fired."}, {WebhookEventWarmupEmailSent, WebhookCatWarmup, "A warmup email was sent."}, {WebhookEventWarmupHealthChanged, WebhookCatWarmup, "A mailbox's warmup health state changed."}, {WebhookEventWarmupPlacementInSpam, WebhookCatWarmup, "A warmup email landed in spam."}, {WebhookEventWarmupQuarantined, WebhookCatWarmup, "A mailbox was quarantined from the warmup pool."}, {WebhookEventWarmupBlocked, WebhookCatWarmup, "A mailbox was blocked from the warmup pool."}, {WebhookEventDeliverabilityBounce, WebhookCatDeliverability, "A bounce was recorded (any source)."}, {WebhookEventDeliverabilityComplaint, WebhookCatDeliverability, "A spam complaint was recorded."}, {WebhookEventInboxEmailReceived, WebhookCatInbox, "A new email arrived in a connected mailbox."}, {WebhookEventInboxEmailUpdated, WebhookCatInbox, "An inbox email changed (read state, labels, category)."}, {WebhookEventInboxEmailDeleted, WebhookCatInbox, "An inbox email was deleted."}, {WebhookEventInboxReplyReceived, WebhookCatInbox, "A human reply was received (including non-campaign replies)."}, {WebhookEventEmailAccountConnected, WebhookCatEmailAccount, "A mailbox was connected."}, {WebhookEventEmailAccountDisconnected, WebhookCatEmailAccount, "A mailbox was disconnected."}, {WebhookEventEmailAccountRemoved, WebhookCatEmailAccount, "A mailbox was removed."}, {WebhookEventEmailAccountError, WebhookCatEmailAccount, "A mailbox hit an auth or sync error."}, {WebhookEventEmailAccountSynced, WebhookCatEmailAccount, "A mailbox finished a sync."}, {WebhookEventEmailAccountHealthChanged, WebhookCatEmailAccount, "A mailbox's health state changed."}, {WebhookEventContactCreated, WebhookCatContact, "A contact was created."}, {WebhookEventContactUpdated, WebhookCatContact, "A contact was updated."}, {WebhookEventContactDeleted, WebhookCatContact, "A contact was deleted."}, {WebhookEventFormSubmitted, WebhookCatContact, "A hosted form received a submission."}, {WebhookEventCRMDealCreated, WebhookCatCRM, "A CRM deal was created."}, {WebhookEventCRMDealUpdated, WebhookCatCRM, "A CRM deal changed (including stage moves)."}, {WebhookEventCRMDealDeleted, WebhookCatCRM, "A CRM deal was deleted."}, {WebhookEventCRMTaskCreated, WebhookCatCRM, "A CRM task was created."}, {WebhookEventCRMTaskUpdated, WebhookCatCRM, "A CRM task changed."}, {WebhookEventCRMNoteCreated, WebhookCatCRM, "A CRM note was added."}, {WebhookEventCRMPipelineChange, WebhookCatCRM, "A CRM pipeline or stage changed."}, {WebhookEventAutomationCreated, WebhookCatAutomation, "An automation was created."}, {WebhookEventAutomationUpdated, WebhookCatAutomation, "An automation was updated."}, {WebhookEventAutomationDeleted, WebhookCatAutomation, "An automation was deleted."}, {WebhookEventAutomationRun, WebhookCatAutomation, "An automation run completed."}, {WebhookEventMeetingBooked, WebhookCatMeeting, "A meeting was booked."}, {WebhookEventMeetingRescheduled, WebhookCatMeeting, "A meeting was rescheduled."}, {WebhookEventMeetingCanceled, WebhookCatMeeting, "A meeting was canceled."}, {WebhookEventTeamMemberInvited, WebhookCatTeam, "A teammate was invited."}, {WebhookEventTeamMemberRemoved, WebhookCatTeam, "A teammate was removed."}, {WebhookEventRoleCreated, WebhookCatTeam, "A role was created."}, {WebhookEventRoleUpdated, WebhookCatTeam, "A role was updated."}, {WebhookEventRoleDeleted, WebhookCatTeam, "A role was deleted."}, {WebhookEventBulkOperationStarted, WebhookCatBulk, "A bulk import/export started."}, {WebhookEventBulkOperationCompleted, WebhookCatBulk, "A bulk import/export completed."}, {WebhookEventBulkOperationFailed, WebhookCatBulk, "A bulk import/export failed."}, {WebhookEventLeadSyncSourceUpdated, WebhookCatWorkspace, "A lead-sync source changed."}, {WebhookEventSettingsUpdated, WebhookCatWorkspace, "Workspace settings changed."}, {WebhookEventSubscriptionUpdated, WebhookCatWorkspace, "The plan or subscription changed."}, {WebhookEventCustom, WebhookCatDeveloper, "A developer-defined custom event was fired."}, } out := make([]WebhookEventDescriptor, 0, len(rows)) for _, r := range rows { out = append(out, WebhookEventDescriptor{ Type: r.t, Category: r.cat, Description: r.desc, Firehose: firehoseEvents[r.t], }) } return out } // WebhookEventRequiredScope returns the API-permission bit an OAuth app must // hold (and the granting org must have granted) to receive a given event via its // app-level webhook subscription. 0 means "no specific scope required". This // mirrors the realtime gateway's per-member event gating so an app can never // receive data its grant did not authorize. func WebhookEventRequiredScope(t WebhookEventType) uint64 { s := string(t) switch { case strings.HasPrefix(s, "inbox."): return APIPermReadUnibox case strings.HasPrefix(s, "campaign."), strings.HasPrefix(s, "deliverability."), strings.HasPrefix(s, "meeting."): return APIPermReadCampaigns case strings.HasPrefix(s, "email_account."), strings.HasPrefix(s, "warmup."): return APIPermReadEmails case strings.HasPrefix(s, "contact."), strings.HasPrefix(s, "bulk_operation."): return APIPermReadContacts case strings.HasPrefix(s, "crm."): return APIPermReadCRM case strings.HasPrefix(s, "automation."): return APIPermIntegrations case strings.HasPrefix(s, "team."), strings.HasPrefix(s, "role."), strings.HasPrefix(s, "settings."), strings.HasPrefix(s, "subscription."): return APIPermReadAuditLogs case strings.HasPrefix(s, "custom."): return APIPermRealtimeSubscribe default: return APIPermReadCampaigns } } // EventAllowedByScopes reports whether a grant holding `scopes` may receive the // given event. func EventAllowedByScopes(t WebhookEventType, scopes uint64) bool { req := WebhookEventRequiredScope(t) return req == 0 || scopes&req != 0 } // AppSubscribedEventTypes resolves the concrete event_types an app-materialized // endpoint should carry for a grant: the app's requested `wanted` events, // filtered to those the grant's `scopes` allow. An empty `wanted` means "all // non-firehose events", still filtered by scope. The result is what the managed // webhook_endpoints row stores, so the normal delivery path enforces scoping. func AppSubscribedEventTypes(wanted []string, scopes uint64) []string { if len(wanted) > 0 { out := make([]string, 0, len(wanted)) for _, w := range wanted { if EventAllowedByScopes(WebhookEventType(w), scopes) { out = append(out, w) } } return out } // Empty filter: every non-firehose event the scopes allow. out := make([]string, 0, len(AllWebhookEventTypes)) for _, t := range AllWebhookEventTypes { if IsFirehoseEvent(t) || t == WebhookEventInboundWebhook { continue } if EventAllowedByScopes(t, scopes) { out = append(out, string(t)) } } return out } // WebhookEventForAudit maps an audit-spine (entity_type, action) pair to the // outbound webhook event it should fan out as, or ("", false) to skip. This is // the bridge that turns every audited mutation into a typed customer webhook // without a bespoke emit at each call site. It deliberately SKIPS: // - operator-internal entities (worker, aws_credentials, worker_profile, // release, api_key, user, organization, integration) // - entities that already have a dedicated, richer emit (email_account, // meeting) to avoid double-delivery // - the webhook entity itself (no self-referential delivery loops) // - noisy low-value entities (folder, tag, category, unibox, step, // warmup_routing_rule) func WebhookEventForAudit(entityType AuditEntityType, action AuditAction) (WebhookEventType, bool) { switch entityType { case AuditEntityCampaign: switch action { case AuditActionCreate: return WebhookEventCampaignCreated, true case AuditActionUpdate: return WebhookEventCampaignUpdated, true case AuditActionDelete: return WebhookEventCampaignDeleted, true case AuditActionStart, AuditActionResume: // User-initiated start/resume. The automated deliverability auto-pause // path emits campaign.paused directly (no audit log), so there is no // double-emit for these. return WebhookEventCampaignStarted, true case AuditActionPause: return WebhookEventCampaignPaused, true } case AuditEntityContact: switch action { case AuditActionCreate: return WebhookEventContactCreated, true case AuditActionUpdate: return WebhookEventContactUpdated, true case AuditActionDelete: return WebhookEventContactDeleted, true } case AuditEntityTemplate: switch action { case AuditActionCreate: return WebhookEventTemplateCreated, true case AuditActionUpdate: return WebhookEventTemplateUpdated, true case AuditActionDelete: return WebhookEventTemplateDeleted, true } case AuditEntityAutomation: switch action { case AuditActionCreate: return WebhookEventAutomationCreated, true case AuditActionUpdate: return WebhookEventAutomationUpdated, true case AuditActionDelete: return WebhookEventAutomationDeleted, true } case AuditEntityRole: switch action { case AuditActionCreate: return WebhookEventRoleCreated, true case AuditActionUpdate: return WebhookEventRoleUpdated, true case AuditActionDelete: return WebhookEventRoleDeleted, true } case AuditEntityCRMDeal: switch action { case AuditActionCreate: return WebhookEventCRMDealCreated, true case AuditActionUpdate: return WebhookEventCRMDealUpdated, true case AuditActionDelete: return WebhookEventCRMDealDeleted, true } case AuditEntityCRMTask: switch action { case AuditActionCreate: return WebhookEventCRMTaskCreated, true case AuditActionUpdate: return WebhookEventCRMTaskUpdated, true } case AuditEntityCRMNote: if action == AuditActionCreate { return WebhookEventCRMNoteCreated, true } case AuditEntityCRMPipeline, AuditEntityCRMStage: return WebhookEventCRMPipelineChange, true case AuditEntityInvitation: switch action { case AuditActionInvite, AuditActionCreate: return WebhookEventTeamMemberInvited, true } case AuditEntityOrganizationMember: if action == AuditActionRemove { return WebhookEventTeamMemberRemoved, true } case AuditEntityLeadSyncSource: switch action { case AuditActionCreate, AuditActionUpdate: return WebhookEventLeadSyncSourceUpdated, true } case AuditEntitySettings: if action == AuditActionUpdate { return WebhookEventSettingsUpdated, true } case AuditEntitySubscription: if action == AuditActionUpdate { return WebhookEventSubscriptionUpdated, true } } return "", false } // WebhookEndpoint is a customer's subscription to events. type WebhookEndpoint struct { ID uuid.UUID `json:"id"` OrganizationID uuid.UUID `json:"organization_id"` URL string `json:"url"` Description string `json:"description"` EventTypes []string `json:"event_types"` Enabled bool `json:"enabled"` LastSuccessAt *time.Time `json:"last_success_at,omitempty"` LastFailureAt *time.Time `json:"last_failure_at,omitempty"` LastFailureReason *string `json:"last_failure_reason,omitempty"` ConsecutiveFailures int `json:"consecutive_failures"` // OAuthApplicationID is set for app-scoped endpoints. Their URL host must // stay within the owning app's allowed_webhook_domains on every write and // delivery; nil for normal org-level endpoints. OAuthApplicationID *uuid.UUID `json:"oauth_application_id,omitempty"` CreatedBy *uuid.UUID `json:"created_by,omitempty"` // VerifiedAt is non-nil once the endpoint proved it accepts our challenge. // Only verified endpoints receive the real event stream. VerifiedAt *time.Time `json:"verified_at,omitempty"` // OwnershipConfirmed is true once the receiver echoed the challenge back // (proves control of the URL, not just reachability). OwnershipConfirmed bool `json:"ownership_confirmed"` AutoDisabledAt *time.Time `json:"auto_disabled_at,omitempty"` DisabledReason *string `json:"disabled_reason,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // Verified reports whether the endpoint has passed ownership verification. func (e *WebhookEndpoint) Verified() bool { return e.VerifiedAt != nil } // WebhookEndpointWithSecret is returned once at creation time so the // client can capture the secret. Subsequent reads do not include it. type WebhookEndpointWithSecret struct { WebhookEndpoint Secret string `json:"secret"` } // Subscribes returns true if this endpoint should receive the given event. An // endpoint must be enabled and verified. An explicit event_types entry always // matches (the only way to opt into high-volume firehose events). An empty // event_types array matches all events EXCEPT firehose ones, which must be // subscribed to explicitly. func (e *WebhookEndpoint) Subscribes(event WebhookEventType) bool { if !e.Enabled || !e.Verified() { return false } for _, t := range e.EventTypes { if t == string(event) { return true } } return len(e.EventTypes) == 0 && !IsFirehoseEvent(event) } // WebhookDeliveryStatus tracks where a delivery attempt sits in its // lifecycle. 'abandoned' = retries exhausted. type WebhookDeliveryStatus string const ( WebhookDeliveryPending WebhookDeliveryStatus = "pending" WebhookDeliveryInFlight WebhookDeliveryStatus = "in_flight" WebhookDeliveryDelivered WebhookDeliveryStatus = "delivered" WebhookDeliveryFailed WebhookDeliveryStatus = "failed" WebhookDeliveryAbandoned WebhookDeliveryStatus = "abandoned" ) // WebhookDelivery is one attempt-history record. Multiple rows may exist // per (event, endpoint) across retries — each row updates in place as // attempts progress. type WebhookDelivery struct { ID uuid.UUID `json:"id"` EndpointID uuid.UUID `json:"endpoint_id"` OrganizationID uuid.UUID `json:"organization_id"` EventType string `json:"event_type"` EventID uuid.UUID `json:"event_id"` Payload json.RawMessage `json:"payload"` Status WebhookDeliveryStatus `json:"status"` AttemptCount int `json:"attempt_count"` MaxAttempts int `json:"max_attempts"` NextAttemptAt time.Time `json:"next_attempt_at"` LastAttemptAt *time.Time `json:"last_attempt_at,omitempty"` ResponseStatus *int `json:"response_status,omitempty"` ResponseBodyExcerpt *string `json:"response_body_excerpt,omitempty"` ErrorReason *string `json:"error_reason,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // WebhookDeliveryFilter narrows a delivery-log query. Empty fields are ignored. type WebhookDeliveryFilter struct { EndpointID *uuid.UUID Status string EventType string Offset int Limit int } // WebhookDeliveriesResult is the paginated delivery-log response (data + // opaque-cursor pagination, matching the platform's list-endpoint contract). type WebhookDeliveriesResult struct { Data []WebhookDelivery `json:"data"` Pagination CPagination `json:"pagination"` } // WebhookEventDrop is a daily rollup of events the dispatch throttle dropped, so // the dashboard can surface rate-limiting instead of it being silent. type WebhookEventDrop struct { EventType string `json:"event_type"` Day time.Time `json:"day"` DroppedWindows int `json:"dropped_windows"` LastDroppedAt time.Time `json:"last_dropped_at"` } // WebhookPayload is the JSON body the dispatcher POSTs to a subscriber. type WebhookPayload struct { ID uuid.UUID `json:"id"` EventType WebhookEventType `json:"event_type"` OrganizationID uuid.UUID `json:"organization_id"` CreatedAt time.Time `json:"created_at"` Data any `json:"data"` }