From d1e3e3676732412eb4df32ff070dea942f2935ec Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 07:58:14 +0100 Subject: [PATCH 1/9] feat: hosted webhook ingress and chat.agent channels --- .changeset/hosted-webhook-ingress.md | 14 + apps/webapp/app/assets/icons/WebhookIcon.tsx | 14 +- .../app/components/navigation/SideMenu.tsx | 25 + .../components/navigation/sideMenuTypes.ts | 1 + .../webapp/app/components/run/RunTimeline.tsx | 6 +- .../app/components/runs/v3/RunFilters.tsx | 3 +- .../components/runs/v3/TaskTriggerSource.tsx | 9 + .../webhookConsole/ReplaySourcePicker.tsx | 94 + .../webhookConsole/SampleSourcePicker.tsx | 200 + .../webhookConsole/WebhookComposer.tsx | 582 + .../webhookDeliveries/v1/DeliveriesTable.tsx | 260 + .../webhookDeliveries/v1/DeliveryStatus.tsx | 38 + .../webhookDeliveries/v1/DeliveryTimeline.tsx | 111 + .../v1/WebhookDeliveryFilters.tsx | 660 + .../v1/buildDeliveryTimelineItems.ts | 125 + .../v1/useDeliveriesLiveReload.ts | 237 + .../webhookEndpoints/v1/EndpointStatus.tsx | 25 + .../webhookEndpoints/v1/EndpointsTable.tsx | 117 + apps/webapp/app/db.server.ts | 58 +- apps/webapp/app/entry.server.tsx | 13 + apps/webapp/app/env.server.ts | 104 + .../v3/ApiWebhookDeliveryPresenter.server.ts | 162 + .../v3/ApiWebhookEndpointPresenter.server.ts | 105 + .../app/presenters/v3/TestPresenter.server.ts | 2 +- .../presenters/v3/TestTaskPresenter.server.ts | 8 +- .../v3/UnifiedTaskListPresenter.server.ts | 9 +- .../WebhookDeliveriesListPresenter.server.ts | 155 + .../WebhookDeliveryDetailPresenter.server.ts | 122 + .../v3/WebhookDetailPresenter.server.ts | 645 + .../v3/webhookComposerEndpoints.server.ts | 53 + .../route.tsx | 68 +- .../AIPayloadTabContent.tsx | 44 +- .../route.tsx | 19 +- .../route.tsx | 830 + .../route.tsx | 239 + .../route.tsx | 406 + .../route.tsx | 732 + .../route.tsx | 10 + apps/webapp/app/routes/api.v1.sessions.ts | 77 +- ....webhooks.deliveries.$deliveryId.replay.ts | 58 + .../api.v1.webhooks.deliveries.$deliveryId.ts | 25 + .../app/routes/api.v1.webhooks.deliveries.ts | 22 + ....webhooks.endpoints.$endpointId.disable.ts | 34 + ...1.webhooks.endpoints.$endpointId.enable.ts | 34 + ...oks.endpoints.$endpointId.rotate-secret.ts | 51 + .../api.v1.webhooks.endpoints.$endpointId.ts | 25 + .../app/routes/api.v1.webhooks.endpoints.ts | 22 + ...env.$envParam.test.ai-generate-payload.tsx | 85 +- ....env.$envParam.webhooks.deliveries.live.ts | 122 + ....endpoints.$endpointParam.replay-source.ts | 148 + ....webhooks.endpoints.$endpointParam.send.ts | 217 + ...ectParam.env.$envParam.webhooks.samples.ts | 106 + .../routes/webhooks.v1.ingest.$opaqueId.ts | 75 + .../clickhouse/clickhouseFactory.server.ts | 33 + .../app/services/realtime/sessions.server.ts | 74 +- .../services/taskIdentifierRegistry.server.ts | 1 + .../webhookConsoleSendRateLimit.server.ts | 14 + ...ookDeliveriesReplicationInstance.server.ts | 92 + ...hookDeliveriesReplicationService.server.ts | 821 + ...houseWebhookDeliveriesRepository.server.ts | 372 + .../webhookDeliveriesRepository.server.ts | 231 + .../webhookIngressIpRateLimit.server.ts | 26 + .../webhookIngressRateLimit.server.ts | 16 + apps/webapp/app/utils/pathBuilder.ts | 46 + .../app/utils/readBodyWithCap.server.ts | 40 + .../app/utils/webhookIngressUrl.server.ts | 11 + apps/webapp/app/v3/featureFlags.ts | 2 + .../app/v3/services/adminWorker.server.ts | 5 + .../changeCurrentDeployment.server.ts | 10 +- .../services/createBackgroundWorker.server.ts | 167 +- ...eateDeploymentBackgroundWorkerV4.server.ts | 20 + apps/webapp/app/v3/webhookEngine.server.ts | 284 + apps/webapp/package.json | 2 + apps/webapp/server.ts | 15 +- apps/webapp/tailwind.config.js | 387 + .../buildDeliveryTimelineItems.test.ts | 106 + apps/webapp/test/readBodyWithCap.test.ts | 45 + docker/Dockerfile.postgres | 5 - docker/dev-compose.yml | 6 +- docker/docker-compose.yml | 10 +- docs/ai-chat/backend.mdx | 31 + docs/ai-chat/reference.mdx | 40 + docs/docs.json | 13 + docs/webhooks/channels.mdx | 133 + docs/webhooks/connect.mdx | 35 + docs/webhooks/deliveries.mdx | 48 + docs/webhooks/filters.mdx | 99 + docs/webhooks/human-in-the-loop.mdx | 143 + docs/webhooks/overview.mdx | 96 + docs/webhooks/session-routing.mdx | 96 + docs/webhooks/sources.mdx | 115 + .../034_create_webhook_deliveries_v1.sql | 33 + internal-packages/clickhouse/src/index.ts | 21 + .../clickhouse/src/webhookDeliveries.ts | 155 + .../migration.sql | 5 + .../migration.sql | 76 + .../migration.sql | 4 + .../migration.sql | 3 + .../migration.sql | 11 + .../database/prisma/schema.prisma | 92 + internal-packages/database/src/transaction.ts | 19 + .../llm-model-catalog/src/defaultPrices.ts | 6169 ++- .../llm-model-catalog/src/modelCatalog.ts | 4059 +- internal-packages/replication/src/client.ts | 18 +- internal-packages/testcontainers/src/index.ts | 6 +- internal-packages/testcontainers/src/utils.ts | 14 + internal-packages/webhook-engine/package.json | 37 + .../webhook-engine/src/engine/cache.ts | 49 + .../src/engine/filter/evaluate.ts | 185 + .../src/engine/filter/filter.test.ts | 218 + .../webhook-engine/src/engine/filter/index.ts | 8 + .../webhook-engine/src/engine/filter/parse.ts | 222 + .../webhook-engine/src/engine/filter/types.ts | 19 + .../webhook-engine/src/engine/index.ts | 830 + .../webhook-engine/src/engine/ingest.test.ts | 772 + .../src/engine/partitions.test.ts | 116 + .../webhook-engine/src/engine/partitions.ts | 245 + .../src/engine/sessionKey.test.ts | 86 + .../webhook-engine/src/engine/sessionKey.ts | 54 + .../src/engine/signing/index.ts | 193 + .../src/engine/signing/signing.test.ts | 169 + .../webhook-engine/src/engine/types.ts | 110 + .../src/engine/verification/asymmetric.ts | 89 + .../src/engine/verification/derive.ts | 60 + .../src/engine/verification/hmac.ts | 54 + .../src/engine/verification/index.ts | 43 + .../src/engine/verification/parse.ts | 136 + .../src/engine/verification/sharedSecret.ts | 64 + .../src/engine/verification/types.ts | 20 + .../src/engine/verification/urlSecret.ts | 38 + .../src/engine/verification/util.ts | 61 + .../src/engine/verification/verify.test.ts | 350 + .../src/engine/workerCatalog.ts | 24 + internal-packages/webhook-engine/src/index.ts | 15 + .../webhook-engine/test/setup.ts | 4 + .../webhook-engine/tsconfig.build.json | 21 + .../webhook-engine/tsconfig.json | 8 + .../webhook-engine/tsconfig.src.json | 20 + .../webhook-engine/tsconfig.test.json | 20 + .../webhook-engine/vitest.config.ts | 17 + internal-packages/webhook-sources/NOTICE.md | 11 + .../catalog/build-aggregates.ts | 52 + .../webhook-sources/catalog/build-brief.md | 118 + .../webhook-sources/catalog/build-v1.ts | 519 + .../webhook-sources/catalog/mark.ts | 76 + .../webhook-sources/catalog/providers.json | 925 + .../catalog/research-notes-wave2.md | 114 + .../catalog/research-notes-wave3.md | 35 + .../webhook-sources/catalog/research-notes.md | 84 + .../webhook-sources/catalog/status.ts | 114 + .../webhook-sources/package.json | 22 + .../src/generated/hookdeck-samples.json | 33048 ++++++++++++++++ .../src/handAuthored/anthropic.ts | 119 + .../src/handAuthored/assemblyai.ts | 39 + .../webhook-sources/src/handAuthored/attio.ts | 145 + .../webhook-sources/src/handAuthored/auth0.ts | 214 + .../webhook-sources/src/handAuthored/brex.ts | 95 + .../src/handAuthored/cal-com.ts | 163 + .../src/handAuthored/calendly.ts | 236 + .../webhook-sources/src/handAuthored/clerk.ts | 39 + .../src/handAuthored/close-crm.ts | 287 + .../src/handAuthored/deepgram.ts | 304 + .../src/handAuthored/discord.ts | 150 + .../src/handAuthored/docusign.ts | 278 + .../src/handAuthored/elevenlabs.ts | 271 + .../src/handAuthored/gitlab.ts | 270 + .../webhook-sources/src/handAuthored/index.ts | 85 + .../src/handAuthored/intercom.ts | 388 + .../webhook-sources/src/handAuthored/jira.ts | 235 + .../src/handAuthored/linear.ts | 197 + .../src/handAuthored/notion.ts | 136 + .../src/handAuthored/openai.ts | 135 + .../src/handAuthored/pagerduty.ts | 355 + .../webhook-sources/src/handAuthored/plaid.ts | 121 + .../src/handAuthored/postmark.ts | 192 + .../src/handAuthored/recall-ai.ts | 141 + .../src/handAuthored/replicate.ts | 191 + .../src/handAuthored/resend.ts | 165 + .../src/handAuthored/retell.ts | 192 + .../src/handAuthored/sendgrid.ts | 125 + .../src/handAuthored/sentry.ts | 260 + .../webhook-sources/src/handAuthored/slack.ts | 196 + .../src/handAuthored/square.ts | 203 + .../src/handAuthored/supabase.ts | 147 + .../src/handAuthored/telegram.ts | 146 + .../src/handAuthored/twilio.ts | 152 + .../src/handAuthored/typeform.ts | 321 + .../webhook-sources/src/handAuthored/vapi.ts | 160 + .../src/handAuthored/vercel.ts | 192 + .../src/handAuthored/whatsapp.ts | 298 + .../src/handAuthored/workos.ts | 167 + .../src/handAuthored/zendesk.ts | 146 + .../webhook-sources/src/handAuthored/zoom.ts | 194 + .../webhook-sources/src/index.ts | 25 + .../webhook-sources/src/ingest.ts | 115 + .../webhook-sources/src/registry/adyen.ts | 11 + .../webhook-sources/src/registry/anthropic.ts | 18 + .../src/registry/assemblyai.ts | 18 + .../webhook-sources/src/registry/attio.ts | 17 + .../webhook-sources/src/registry/auth0.ts | 18 + .../src/registry/bigcommerce.ts | 11 + .../webhook-sources/src/registry/bitbucket.ts | 11 + .../webhook-sources/src/registry/brex.ts | 19 + .../webhook-sources/src/registry/cal-com.ts | 16 + .../webhook-sources/src/registry/calendly.ts | 16 + .../src/registry/categories.ts | 45 + .../webhook-sources/src/registry/checkout.ts | 11 + .../webhook-sources/src/registry/clerk.ts | 12 + .../webhook-sources/src/registry/close-crm.ts | 21 + .../src/registry/commercelayer.ts | 11 + .../webhook-sources/src/registry/deepgram.ts | 22 + .../webhook-sources/src/registry/discord.ts | 19 + .../webhook-sources/src/registry/docusign.ts | 11 + .../src/registry/elevenlabs.ts | 17 + .../webhook-sources/src/registry/github.ts | 12 + .../webhook-sources/src/registry/gitlab.ts | 12 + .../webhook-sources/src/registry/hubspot.ts | 11 + .../webhook-sources/src/registry/index.ts | 123 + .../webhook-sources/src/registry/intercom.ts | 18 + .../webhook-sources/src/registry/jira.ts | 16 + .../webhook-sources/src/registry/linear.ts | 19 + .../webhook-sources/src/registry/monday.ts | 11 + .../webhook-sources/src/registry/notion.ts | 11 + .../webhook-sources/src/registry/openai.ts | 12 + .../src/registry/paddlebilling.ts | 11 + .../src/registry/paddleclassic.ts | 11 + .../webhook-sources/src/registry/pagerduty.ts | 18 + .../webhook-sources/src/registry/paypal.ts | 11 + .../webhook-sources/src/registry/pipedrive.ts | 11 + .../webhook-sources/src/registry/plaid.ts | 21 + .../webhook-sources/src/registry/postmark.ts | 18 + .../webhook-sources/src/registry/recall-ai.ts | 16 + .../webhook-sources/src/registry/replicate.ts | 12 + .../webhook-sources/src/registry/resend.ts | 12 + .../webhook-sources/src/registry/retell.ts | 16 + .../webhook-sources/src/registry/sendgrid.ts | 20 + .../webhook-sources/src/registry/sentry.ts | 17 + .../webhook-sources/src/registry/shopify.ts | 11 + .../webhook-sources/src/registry/slack.ts | 11 + .../webhook-sources/src/registry/square.ts | 12 + .../webhook-sources/src/registry/stripe.ts | 12 + .../webhook-sources/src/registry/supabase.ts | 19 + .../webhook-sources/src/registry/telegram.ts | 19 + .../webhook-sources/src/registry/twilio.ts | 21 + .../webhook-sources/src/registry/typeform.ts | 17 + .../webhook-sources/src/registry/types.ts | 32 + .../webhook-sources/src/registry/vapi.ts | 18 + .../webhook-sources/src/registry/vercel.ts | 18 + .../webhook-sources/src/registry/whatsapp.ts | 20 + .../src/registry/woocommerce.ts | 11 + .../webhook-sources/src/registry/workos.ts | 16 + .../webhook-sources/src/registry/zendesk.ts | 24 + .../webhook-sources/src/registry/zoom.ts | 11 + .../webhook-sources/src/roundtrip.test.ts | 95 + .../webhook-sources/src/sampleRecord.ts | 36 + .../webhook-sources/src/samples.ts | 16 + .../webhook-sources/tsconfig.json | 22 + .../webhook-sources/vitest.config.ts | 11 + packages/cli-v3/src/dev/devSupervisor.ts | 13 +- .../src/entryPoints/dev-index-worker.ts | 2 + .../src/entryPoints/managed-index-worker.ts | 2 + packages/core/package.json | 12 + packages/core/src/v3/isomorphic/friendlyId.ts | 2 + .../core/src/v3/resource-catalog/catalog.ts | 11 + .../core/src/v3/resource-catalog/index.ts | 24 + .../resource-catalog/noopResourceCatalog.ts | 24 + .../standardResourceCatalog.ts | 71 + packages/core/src/v3/schemas/build.ts | 11 +- packages/core/src/v3/schemas/index.ts | 3 + packages/core/src/v3/schemas/resources.ts | 20 + packages/core/src/v3/schemas/schemas.ts | 27 + packages/core/src/v3/schemas/webhookApi.ts | 106 + packages/core/src/v3/schemas/webhookConfig.ts | 214 + packages/core/src/v3/schemas/webhookFilter.ts | 278 + packages/core/src/v3/schemas/webhooks.ts | 5 + packages/core/src/v3/types/chatEvents.ts | 112 + packages/core/src/v3/types/index.ts | 2 + packages/core/src/v3/types/webhooks.ts | 39 + packages/core/src/v3/webhooks/index.ts | 295 + packages/slack/package.json | 74 + packages/slack/src/index.test.ts | 307 + packages/slack/src/index.ts | 409 + packages/slack/tsconfig.json | 8 + packages/slack/tsconfig.src.json | 10 + packages/slack/vitest.config.ts | 8 + packages/trigger-sdk/src/v3/ai.ts | 749 +- .../src/v3/channelReactions.test.ts | 39 + packages/trigger-sdk/src/v3/chat.ts | 18 + packages/trigger-sdk/src/v3/webhooks.ts | 327 +- pnpm-lock.yaml | 364 +- 290 files changed, 68929 insertions(+), 5118 deletions(-) create mode 100644 .changeset/hosted-webhook-ingress.md create mode 100644 apps/webapp/app/components/webhookConsole/ReplaySourcePicker.tsx create mode 100644 apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx create mode 100644 apps/webapp/app/components/webhookConsole/WebhookComposer.tsx create mode 100644 apps/webapp/app/components/webhookDeliveries/v1/DeliveriesTable.tsx create mode 100644 apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx create mode 100644 apps/webapp/app/components/webhookDeliveries/v1/DeliveryTimeline.tsx create mode 100644 apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx create mode 100644 apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts create mode 100644 apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts create mode 100644 apps/webapp/app/components/webhookEndpoints/v1/EndpointStatus.tsx create mode 100644 apps/webapp/app/components/webhookEndpoints/v1/EndpointsTable.tsx create mode 100644 apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts create mode 100644 apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts create mode 100644 apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts create mode 100644 apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts create mode 100644 apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts create mode 100644 apps/webapp/app/presenters/v3/webhookComposerEndpoints.server.ts create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks._index/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.$deliveryParam/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx create mode 100644 apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks/route.tsx create mode 100644 apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.deliveries.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.ts create mode 100644 apps/webapp/app/routes/api.v1.webhooks.endpoints.ts create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.replay-source.ts create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.send.ts create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts create mode 100644 apps/webapp/app/routes/webhooks.v1.ingest.$opaqueId.ts create mode 100644 apps/webapp/app/services/webhookConsoleSendRateLimit.server.ts create mode 100644 apps/webapp/app/services/webhookDeliveriesReplicationInstance.server.ts create mode 100644 apps/webapp/app/services/webhookDeliveriesReplicationService.server.ts create mode 100644 apps/webapp/app/services/webhookDeliveriesRepository/clickhouseWebhookDeliveriesRepository.server.ts create mode 100644 apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts create mode 100644 apps/webapp/app/services/webhookIngressIpRateLimit.server.ts create mode 100644 apps/webapp/app/services/webhookIngressRateLimit.server.ts create mode 100644 apps/webapp/app/utils/readBodyWithCap.server.ts create mode 100644 apps/webapp/app/utils/webhookIngressUrl.server.ts create mode 100644 apps/webapp/app/v3/webhookEngine.server.ts create mode 100644 apps/webapp/tailwind.config.js create mode 100644 apps/webapp/test/components/webhookDeliveries/buildDeliveryTimelineItems.test.ts create mode 100644 apps/webapp/test/readBodyWithCap.test.ts delete mode 100644 docker/Dockerfile.postgres create mode 100644 docs/webhooks/channels.mdx create mode 100644 docs/webhooks/connect.mdx create mode 100644 docs/webhooks/deliveries.mdx create mode 100644 docs/webhooks/filters.mdx create mode 100644 docs/webhooks/human-in-the-loop.mdx create mode 100644 docs/webhooks/overview.mdx create mode 100644 docs/webhooks/session-routing.mdx create mode 100644 docs/webhooks/sources.mdx create mode 100644 internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql create mode 100644 internal-packages/clickhouse/src/webhookDeliveries.ts create mode 100644 internal-packages/database/prisma/migrations/20260622120730_add_webhook_trigger_source/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20260622120731_add_webhook_endpoint_and_delivery/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20260622130000_add_webhook_delivery_headers/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20260622140000_add_webhook_endpoint_secret_provisioning/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20260630120000_add_webhook_delivery_filter/migration.sql create mode 100644 internal-packages/webhook-engine/package.json create mode 100644 internal-packages/webhook-engine/src/engine/cache.ts create mode 100644 internal-packages/webhook-engine/src/engine/filter/evaluate.ts create mode 100644 internal-packages/webhook-engine/src/engine/filter/filter.test.ts create mode 100644 internal-packages/webhook-engine/src/engine/filter/index.ts create mode 100644 internal-packages/webhook-engine/src/engine/filter/parse.ts create mode 100644 internal-packages/webhook-engine/src/engine/filter/types.ts create mode 100644 internal-packages/webhook-engine/src/engine/index.ts create mode 100644 internal-packages/webhook-engine/src/engine/ingest.test.ts create mode 100644 internal-packages/webhook-engine/src/engine/partitions.test.ts create mode 100644 internal-packages/webhook-engine/src/engine/partitions.ts create mode 100644 internal-packages/webhook-engine/src/engine/sessionKey.test.ts create mode 100644 internal-packages/webhook-engine/src/engine/sessionKey.ts create mode 100644 internal-packages/webhook-engine/src/engine/signing/index.ts create mode 100644 internal-packages/webhook-engine/src/engine/signing/signing.test.ts create mode 100644 internal-packages/webhook-engine/src/engine/types.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/asymmetric.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/derive.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/hmac.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/index.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/parse.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/sharedSecret.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/types.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/urlSecret.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/util.ts create mode 100644 internal-packages/webhook-engine/src/engine/verification/verify.test.ts create mode 100644 internal-packages/webhook-engine/src/engine/workerCatalog.ts create mode 100644 internal-packages/webhook-engine/src/index.ts create mode 100644 internal-packages/webhook-engine/test/setup.ts create mode 100644 internal-packages/webhook-engine/tsconfig.build.json create mode 100644 internal-packages/webhook-engine/tsconfig.json create mode 100644 internal-packages/webhook-engine/tsconfig.src.json create mode 100644 internal-packages/webhook-engine/tsconfig.test.json create mode 100644 internal-packages/webhook-engine/vitest.config.ts create mode 100644 internal-packages/webhook-sources/NOTICE.md create mode 100644 internal-packages/webhook-sources/catalog/build-aggregates.ts create mode 100644 internal-packages/webhook-sources/catalog/build-brief.md create mode 100644 internal-packages/webhook-sources/catalog/build-v1.ts create mode 100644 internal-packages/webhook-sources/catalog/mark.ts create mode 100644 internal-packages/webhook-sources/catalog/providers.json create mode 100644 internal-packages/webhook-sources/catalog/research-notes-wave2.md create mode 100644 internal-packages/webhook-sources/catalog/research-notes-wave3.md create mode 100644 internal-packages/webhook-sources/catalog/research-notes.md create mode 100644 internal-packages/webhook-sources/catalog/status.ts create mode 100644 internal-packages/webhook-sources/package.json create mode 100644 internal-packages/webhook-sources/src/generated/hookdeck-samples.json create mode 100644 internal-packages/webhook-sources/src/handAuthored/anthropic.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/assemblyai.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/attio.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/auth0.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/brex.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/cal-com.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/calendly.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/clerk.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/close-crm.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/deepgram.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/discord.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/docusign.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/elevenlabs.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/gitlab.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/index.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/intercom.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/jira.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/linear.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/notion.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/openai.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/pagerduty.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/plaid.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/postmark.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/recall-ai.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/replicate.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/resend.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/retell.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/sendgrid.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/sentry.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/slack.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/square.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/supabase.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/telegram.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/twilio.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/typeform.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/vapi.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/vercel.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/whatsapp.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/workos.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/zendesk.ts create mode 100644 internal-packages/webhook-sources/src/handAuthored/zoom.ts create mode 100644 internal-packages/webhook-sources/src/index.ts create mode 100644 internal-packages/webhook-sources/src/ingest.ts create mode 100644 internal-packages/webhook-sources/src/registry/adyen.ts create mode 100644 internal-packages/webhook-sources/src/registry/anthropic.ts create mode 100644 internal-packages/webhook-sources/src/registry/assemblyai.ts create mode 100644 internal-packages/webhook-sources/src/registry/attio.ts create mode 100644 internal-packages/webhook-sources/src/registry/auth0.ts create mode 100644 internal-packages/webhook-sources/src/registry/bigcommerce.ts create mode 100644 internal-packages/webhook-sources/src/registry/bitbucket.ts create mode 100644 internal-packages/webhook-sources/src/registry/brex.ts create mode 100644 internal-packages/webhook-sources/src/registry/cal-com.ts create mode 100644 internal-packages/webhook-sources/src/registry/calendly.ts create mode 100644 internal-packages/webhook-sources/src/registry/categories.ts create mode 100644 internal-packages/webhook-sources/src/registry/checkout.ts create mode 100644 internal-packages/webhook-sources/src/registry/clerk.ts create mode 100644 internal-packages/webhook-sources/src/registry/close-crm.ts create mode 100644 internal-packages/webhook-sources/src/registry/commercelayer.ts create mode 100644 internal-packages/webhook-sources/src/registry/deepgram.ts create mode 100644 internal-packages/webhook-sources/src/registry/discord.ts create mode 100644 internal-packages/webhook-sources/src/registry/docusign.ts create mode 100644 internal-packages/webhook-sources/src/registry/elevenlabs.ts create mode 100644 internal-packages/webhook-sources/src/registry/github.ts create mode 100644 internal-packages/webhook-sources/src/registry/gitlab.ts create mode 100644 internal-packages/webhook-sources/src/registry/hubspot.ts create mode 100644 internal-packages/webhook-sources/src/registry/index.ts create mode 100644 internal-packages/webhook-sources/src/registry/intercom.ts create mode 100644 internal-packages/webhook-sources/src/registry/jira.ts create mode 100644 internal-packages/webhook-sources/src/registry/linear.ts create mode 100644 internal-packages/webhook-sources/src/registry/monday.ts create mode 100644 internal-packages/webhook-sources/src/registry/notion.ts create mode 100644 internal-packages/webhook-sources/src/registry/openai.ts create mode 100644 internal-packages/webhook-sources/src/registry/paddlebilling.ts create mode 100644 internal-packages/webhook-sources/src/registry/paddleclassic.ts create mode 100644 internal-packages/webhook-sources/src/registry/pagerduty.ts create mode 100644 internal-packages/webhook-sources/src/registry/paypal.ts create mode 100644 internal-packages/webhook-sources/src/registry/pipedrive.ts create mode 100644 internal-packages/webhook-sources/src/registry/plaid.ts create mode 100644 internal-packages/webhook-sources/src/registry/postmark.ts create mode 100644 internal-packages/webhook-sources/src/registry/recall-ai.ts create mode 100644 internal-packages/webhook-sources/src/registry/replicate.ts create mode 100644 internal-packages/webhook-sources/src/registry/resend.ts create mode 100644 internal-packages/webhook-sources/src/registry/retell.ts create mode 100644 internal-packages/webhook-sources/src/registry/sendgrid.ts create mode 100644 internal-packages/webhook-sources/src/registry/sentry.ts create mode 100644 internal-packages/webhook-sources/src/registry/shopify.ts create mode 100644 internal-packages/webhook-sources/src/registry/slack.ts create mode 100644 internal-packages/webhook-sources/src/registry/square.ts create mode 100644 internal-packages/webhook-sources/src/registry/stripe.ts create mode 100644 internal-packages/webhook-sources/src/registry/supabase.ts create mode 100644 internal-packages/webhook-sources/src/registry/telegram.ts create mode 100644 internal-packages/webhook-sources/src/registry/twilio.ts create mode 100644 internal-packages/webhook-sources/src/registry/typeform.ts create mode 100644 internal-packages/webhook-sources/src/registry/types.ts create mode 100644 internal-packages/webhook-sources/src/registry/vapi.ts create mode 100644 internal-packages/webhook-sources/src/registry/vercel.ts create mode 100644 internal-packages/webhook-sources/src/registry/whatsapp.ts create mode 100644 internal-packages/webhook-sources/src/registry/woocommerce.ts create mode 100644 internal-packages/webhook-sources/src/registry/workos.ts create mode 100644 internal-packages/webhook-sources/src/registry/zendesk.ts create mode 100644 internal-packages/webhook-sources/src/registry/zoom.ts create mode 100644 internal-packages/webhook-sources/src/roundtrip.test.ts create mode 100644 internal-packages/webhook-sources/src/sampleRecord.ts create mode 100644 internal-packages/webhook-sources/src/samples.ts create mode 100644 internal-packages/webhook-sources/tsconfig.json create mode 100644 internal-packages/webhook-sources/vitest.config.ts create mode 100644 packages/core/src/v3/schemas/webhookApi.ts create mode 100644 packages/core/src/v3/schemas/webhookConfig.ts create mode 100644 packages/core/src/v3/schemas/webhookFilter.ts create mode 100644 packages/core/src/v3/types/chatEvents.ts create mode 100644 packages/core/src/v3/types/webhooks.ts create mode 100644 packages/core/src/v3/webhooks/index.ts create mode 100644 packages/slack/package.json create mode 100644 packages/slack/src/index.test.ts create mode 100644 packages/slack/src/index.ts create mode 100644 packages/slack/tsconfig.json create mode 100644 packages/slack/tsconfig.src.json create mode 100644 packages/slack/vitest.config.ts create mode 100644 packages/trigger-sdk/src/v3/channelReactions.test.ts diff --git a/.changeset/hosted-webhook-ingress.md b/.changeset/hosted-webhook-ingress.md new file mode 100644 index 00000000000..62363d2b835 --- /dev/null +++ b/.changeset/hosted-webhook-ingress.md @@ -0,0 +1,14 @@ +--- +"@trigger.dev/core": patch +"@trigger.dev/sdk": patch +"@trigger.dev/slack": patch +"trigger.dev": patch +--- + +Add hosted webhooks: receive and verify provider webhooks as a task, with no ingress or verification code of your own. + +- `webhook()` declares an endpoint that routes a verified, typed event to an `onEvent` handler. Choose a source with a preset (`webhooks.stripe()`, `webhooks.github()`, and others) or `webhooks.custom(config)`. Declared webhooks are discovered like tasks and synced to a hosted URL on deploy. +- `filter` gates which deliveries run, using a type-safe expression checked against the event at author time (`event.`/`header.`/`webhook.` paths, `&&`/`||`, comparison and `in`/`contains` operators, field-to-field comparison, and array quantifiers). A non-matching delivery is still recorded, not routed. +- `chat.event({ source, key, type })` routes deliveries that share a `key` to one durable session (per customer, installation, or issue) and delivers them to an agent's `onAction` as a typed envelope. +- Channels turn a chat surface into an agent frontend: `chat.channels.custom({ source, key, inbound, send })`, or the new `@trigger.dev/slack` package's `slack()` (Slack Events API verification, per-thread sessions, `chat.postMessage`/`chat.update` egress, `mentions()`, `startOn`, lifecycle reactions). Inbound messages run as turns and the reply posts back. Human-in-the-loop is built in: a tool with no `execute` pauses the turn, the connector posts controls (Slack ships Approve / Deny buttons), and a verified click resolves the tool and resumes the run. +- HTTP API for listing webhook endpoints and deliveries, plus rotate-secret, enable/disable, and replay. diff --git a/apps/webapp/app/assets/icons/WebhookIcon.tsx b/apps/webapp/app/assets/icons/WebhookIcon.tsx index cbd6e85dddf..7841fe39072 100644 --- a/apps/webapp/app/assets/icons/WebhookIcon.tsx +++ b/apps/webapp/app/assets/icons/WebhookIcon.tsx @@ -1,11 +1,15 @@ export function WebhookIcon({ className }: { className?: string }) { return ( - + + + + ); diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index 31001d05f75..c26a5e9f5ca 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -4,6 +4,7 @@ import { ExclamationTriangleIcon, } from "@heroicons/react/24/outline"; import { useFetcher, useNavigation, useSubmit } from "@remix-run/react"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; import { LayoutGroup, motion } from "framer-motion"; import { type CSSProperties, @@ -119,6 +120,7 @@ import { v3SessionsPath, v3UsagePath, v3WaitpointTokensPath, + v3WebhooksPath, } from "~/utils/pathBuilder"; import { FreePlanUsage } from "../billing/FreePlanUsage"; import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence"; @@ -920,6 +922,29 @@ export function SideMenu({ )} + {(user.admin || user.isImpersonating || featureFlags.hasWebhooksAccess) && ( + + + + )} + ; } + case "WEBHOOK": { + return ( + + ); + } } } @@ -35,5 +41,8 @@ export function taskTriggerSourceDescription(source: TaskTriggerSource) { case "AGENT": { return "Agent task"; } + case "WEBHOOK": { + return "Webhook task"; + } } } diff --git a/apps/webapp/app/components/webhookConsole/ReplaySourcePicker.tsx b/apps/webapp/app/components/webhookConsole/ReplaySourcePicker.tsx new file mode 100644 index 00000000000..4ee147edb61 --- /dev/null +++ b/apps/webapp/app/components/webhookConsole/ReplaySourcePicker.tsx @@ -0,0 +1,94 @@ +import { type WebhookDeliveryStatus } from "@trigger.dev/database"; +import { useEffect } from "react"; +import { useTypedFetcher } from "remix-typedjson"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Spinner } from "~/components/primitives/Spinner"; +import { DeliveryStatusBadge } from "~/components/webhookDeliveries/v1/DeliveryStatus"; +import { type loader as replaySourceLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.replay-source"; + +export function ReplaySourcePicker({ + replaySourcePath, + onLoad, +}: { + replaySourcePath: string; + onLoad: (body: string, headers: Record) => void; +}) { + const listFetcher = useTypedFetcher(); + const payloadFetcher = useTypedFetcher(); + + useEffect(() => { + if (listFetcher.state === "idle" && listFetcher.data === undefined) { + listFetcher.load(replaySourcePath); + } + }, [listFetcher, replaySourcePath]); + + useEffect(() => { + const data = payloadFetcher.data; + if (data?.kind === "payload") { + onLoad(data.body, data.headers); + } + }, [payloadFetcher.data, onLoad]); + + const listLoading = + listFetcher.state === "loading" || + (listFetcher.data === undefined && listFetcher.state !== "idle"); + const list = listFetcher.data?.kind === "list" ? listFetcher.data.deliveries : []; + const loadingDeliveryId = + payloadFetcher.state !== "idle" + ? (new URLSearchParams(payloadFetcher.formAction?.split("?")[1] ?? "").get("deliveryId") ?? + undefined) + : undefined; + + function selectDelivery(friendlyId: string) { + payloadFetcher.load(`${replaySourcePath}?deliveryId=${encodeURIComponent(friendlyId)}`); + } + + return ( +
+
+ + Load a past delivery's payload into the composer + + {listLoading ? : null} +
+
+ {!listLoading && list.length === 0 ? ( +

+ No past deliveries to replay yet. +

+ ) : ( + list.map((delivery) => ( + + )) + )} +
+
+ ); +} diff --git a/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx b/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx new file mode 100644 index 00000000000..0ffd629f842 --- /dev/null +++ b/apps/webapp/app/components/webhookConsole/SampleSourcePicker.tsx @@ -0,0 +1,200 @@ +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import { useEffect, useMemo, useState } from "react"; +import { useTypedFetcher } from "remix-typedjson"; +import { Input } from "~/components/primitives/Input"; +import { Spinner } from "~/components/primitives/Spinner"; +import { cn } from "~/utils/cn"; +import { + type WebhookProviderMeta, + type WebhookSampleMeta, + type loader as samplesLoader, +} from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples"; + +export function SampleSourcePicker({ + samplesPath, + endpointSource, + onLoad, +}: { + samplesPath: string; + /** The endpoint's provider (e.g. "github"); pre-selected in the producer list. */ + endpointSource?: string; + onLoad: (body: string, headers: Record) => void; +}) { + const listFetcher = useTypedFetcher(); + const bodyFetcher = useTypedFetcher(); + const [selectedProvider, setSelectedProvider] = useState(null); + const [producerQuery, setProducerQuery] = useState(""); + const [topicQuery, setTopicQuery] = useState(""); + + useEffect(() => { + if (listFetcher.state === "idle" && listFetcher.data === undefined) { + listFetcher.load(samplesPath); + } + }, [listFetcher, samplesPath]); + + useEffect(() => { + const data = bodyFetcher.data; + if (data?.kind === "body") { + onLoad(data.body, data.extraHeaders ?? {}); + } + }, [bodyFetcher.data, onLoad]); + + const manifest = listFetcher.data?.kind === "manifest" ? listFetcher.data : undefined; + const providers = manifest?.providers ?? []; + const samples = manifest?.samples ?? []; + const listLoading = listFetcher.data === undefined && listFetcher.state !== "idle"; + + useEffect(() => { + if (providers.length === 0) return; + setSelectedProvider((current) => { + if (current && providers.some((p) => p.id === current)) return current; + if (endpointSource && providers.some((p) => p.id === endpointSource)) return endpointSource; + return null; + }); + }, [providers, endpointSource]); + + const filteredProviders = useMemo(() => { + const query = producerQuery.trim().toLowerCase(); + if (!query) return providers; + return providers.filter( + (p) => p.label.toLowerCase().includes(query) || (p.category ?? "").includes(query) + ); + }, [providers, producerQuery]); + + const groupedProviders = useMemo(() => { + const groups = new Map(); + for (const provider of filteredProviders) { + const key = provider.category ?? "other"; + const group = groups.get(key) ?? []; + group.push(provider); + groups.set(key, group); + } + return [...groups.entries()]; + }, [filteredProviders]); + + const events = samples + .filter((item) => item.provider === selectedProvider) + .filter((item) => { + const query = topicQuery.trim().toLowerCase(); + return !query || item.eventType.toLowerCase().includes(query); + }); + + const loadingEventType = + bodyFetcher.state !== "idle" + ? (new URLSearchParams(bodyFetcher.formAction?.split("?")[1] ?? "").get("eventType") ?? + undefined) + : undefined; + + function selectEvent(item: WebhookSampleMeta) { + const query = new URLSearchParams({ provider: item.provider, eventType: item.eventType }); + bodyFetcher.load(`${samplesPath}?${query.toString()}`); + } + + return ( +
+
+
+ +
+ {listLoading ? ( +
+ +
+ ) : filteredProviders.length === 0 ? ( +

No providers

+ ) : ( + groupedProviders.map(([category, group]) => ( +
+

+ {group[0]?.categoryLabel ?? category} +

+ {group.map((provider) => ( + + ))} +
+ )) + )} +
+
+ +
+ +
+ {!selectedProvider ? ( +

+ Select a producer to see its events. +

+ ) : events.length === 0 ? ( +

No matching events

+ ) : ( + events.map((item) => ( + + )) + )} +
+
+
+
+ Curated event payloads. Signed with this endpoint's config at send time. +
+
+ ); +} + +function SearchInput({ + placeholder, + value, + onChange, +}: { + placeholder: string; + value: string; + onChange: (value: string) => void; +}) { + return ( +
+ onChange(event.target.value)} + /> +
+ ); +} diff --git a/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx b/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx new file mode 100644 index 00000000000..c8dc0fbf2e7 --- /dev/null +++ b/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx @@ -0,0 +1,582 @@ +import { PlusIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { useFetcher } from "@remix-run/react"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { CodeBlock } from "~/components/code/CodeBlock"; +import { JSONEditor } from "~/components/code/JSONEditor"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; +import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; +import { Hint } from "~/components/primitives/Hint"; +import { Input } from "~/components/primitives/Input"; +import { InputGroup } from "~/components/primitives/InputGroup"; +import { Label } from "~/components/primitives/Label"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "~/components/primitives/Resizable"; +import { Select, SelectItem } from "~/components/primitives/Select"; +import { TabButton, TabContainer } from "~/components/primitives/Tabs"; +import { cn } from "~/utils/cn"; +import type { WebhookSendResult } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.send"; +import { AIPayloadTabContent } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent"; +import { ReplaySourcePicker } from "./ReplaySourcePicker"; +import { SampleSourcePicker } from "./SampleSourcePicker"; + +type SourceTab = "body" | "sample" | "replay" | "ai"; + +export type WebhookComposerEndpoint = { + friendlyId: string; + label: string; + source: string; + ingressUrl: string; + scheme: "hmac" | "shared-secret" | "url-secret" | "asymmetric"; + hasSigningSecret: boolean; + handshake: { matchPath: string; matchValue: string; respondPath: string } | null; +}; + +export type WebhookComposerProps = { + endpoints: WebhookComposerEndpoint[]; + organizationSlug: string; + projectSlug: string; + environmentSlug: string; + isDevEnvironment: boolean; + environmentLabel: string; + defaultBody?: string; + /** When false, a successful send stays put and shows the inline result strip instead of + * redirecting to the delivery detail page (the console tab keeps its live feed alongside). */ + redirectOnSuccess?: boolean; +}; + +type SignatureMode = "signed" | "unsigned" | "tampered" | "simulate"; + +type HeaderRow = { id: string; key: string; value: string }; + +const DEFAULT_BODY = JSON.stringify({ message: "hello from the webhook console" }, null, 2); + +export function WebhookComposer({ + endpoints, + organizationSlug, + projectSlug, + environmentSlug, + isDevEnvironment, + environmentLabel, + defaultBody, + redirectOnSuccess = true, +}: WebhookComposerProps) { + const fetcher = useFetcher(); + const isSending = fetcher.state !== "idle"; + + const [endpointId, setEndpointId] = useState(endpoints[0]?.friendlyId ?? ""); + const [sourceTab, setSourceTab] = useState("body"); + const [bodyDefault, setBodyDefault] = useState(defaultBody ?? DEFAULT_BODY); + const bodyRef = useRef(bodyDefault); + const [headerRows, setHeaderRows] = useState([]); + const headerIdRef = useRef(0); + + const newHeaderRow = useCallback( + (key = "", value = ""): HeaderRow => ({ id: String(headerIdRef.current++), key, value }), + [] + ); + + const endpoint = useMemo( + () => endpoints.find((e) => e.friendlyId === endpointId) ?? endpoints[0], + [endpoints, endpointId] + ); + + const applyPayload = useCallback( + (body: string, headers: Record) => { + setBodyDefault(body); + bodyRef.current = body; + const entries = Object.entries(headers); + if (entries.length > 0) { + setHeaderRows(entries.map(([key, value]) => newHeaderRow(key, value))); + } + setSourceTab("body"); + }, + [newHeaderRow] + ); + + const signedAvailable = Boolean( + endpoint && endpoint.scheme !== "asymmetric" && endpoint.hasSigningSecret + ); + const signedDisabledReason = !endpoint + ? undefined + : endpoint.scheme === "asymmetric" + ? "This endpoint uses asymmetric signatures, which cannot be produced here." + : !endpoint.hasSigningSecret + ? "Set a signing secret on this endpoint first." + : undefined; + + const [signatureMode, setSignatureMode] = useState( + signedAvailable ? "signed" : "simulate" + ); + + const endpointBasePath = endpoint + ? `/resources/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/webhooks/endpoints/${endpoint.friendlyId}` + : ""; + const sendPath = endpointBasePath ? `${endpointBasePath}/send` : ""; + const replaySourcePath = endpointBasePath ? `${endpointBasePath}/replay-source` : ""; + const samplesPath = `/resources/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/webhooks/samples`; + + function submit(override?: { body?: string; signatureMode?: SignatureMode }) { + if (!endpoint) return; + const headers: Record = {}; + for (const row of headerRows) { + const key = row.key.trim(); + if (key) headers[key] = row.value; + } + fetcher.submit( + { + body: override?.body ?? bodyRef.current, + headers, + signatureMode: override?.signatureMode ?? signatureMode, + redirect: redirectOnSuccess, + }, + { method: "post", action: sendPath, encType: "application/json" } + ); + } + + function sendHandshake() { + if (!endpoint?.handshake) return; + const challenge = `chal_${Math.random().toString(36).slice(2, 10)}`; + const body = JSON.stringify(buildHandshakeBody(endpoint.handshake, challenge), null, 2); + applyPayload(body, {}); + submit({ body, signatureMode: "signed" }); + } + + const result = fetcher.data; + const deliveryPath = + result?.success && result.deliveryId?.startsWith("whd_") + ? `/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/webhooks/deliveries/${result.deliveryId}` + : undefined; + + return ( +
+ + +
+
+ + setSourceTab("body")} + > + Body + + setSourceTab("sample")} + > + Library + + setSourceTab("replay")} + > + Replay + + setSourceTab("ai")} + > + AI + + +
+
+
+ { + bodyRef.current = v; + }} + height="100%" + className="h-full overflow-auto" + showClearButton={false} + additionalActions={ + Event body + } + /> +
+ {sourceTab === "sample" ? ( +
+ +
+ ) : null} + {sourceTab === "replay" ? ( +
+ +
+ ) : null} + {sourceTab === "ai" ? ( +
+ applyPayload(payload, {})} + taskIdentifier={endpoint?.source ?? "webhook"} + payloadKind="webhook" + providerSource={endpoint?.source} + generateButtonLabel="Generate event" + placeholder="e.g. a payment succeeded event with a $42.00 charge" + /> +
+ ) : null} +
+ {result ? ( +
+ +
+ ) : null} +
+
+ + + + +
+ {!isDevEnvironment ? ( + + Sends a real delivery through the {environmentLabel} endpoint and triggers a real + run. + + ) : null} + + {endpoints.length > 1 ? ( + + + + + ) : null} + + + + + {signatureMode === "signed" && signedDisabledReason ? ( + {signedDisabledReason} + ) : signatureMode === "signed" ? ( + Signed server-side with the endpoint's stored secret. + ) : signatureMode === "simulate" ? ( + + Injected via the engine, skipping signature verification. Filter, startOn, + routing, and the run all still execute. + + ) : ( + The delivery is rejected fail-closed; no delivery row is written. + )} + + + + + setHeaderRows((rows) => [...rows, newHeaderRow()])} + /> + + Optional provider routing headers (e.g. x-github-event). The signature header is + added automatically. + + + + {endpoint ? ( + + + + POST + + } + /> + + The public URL providers POST to. Test sends run the same pipeline in-process. + + + ) : null} +
+
+
+ +
+ + {isDevEnvironment + ? "Signs with the endpoint's secret and runs the full delivery pipeline." + : `Sends through the ${environmentLabel} endpoint.`} + +
+ {endpoint?.handshake && signedAvailable ? ( + + ) : null} + {isDevEnvironment ? ( + + ) : ( + submit()} + /> + )} +
+
+
+ ); +} + +function HeadersEditor({ + rows, + onChange, + onAdd, +}: { + rows: HeaderRow[]; + onChange: (rows: HeaderRow[]) => void; + onAdd: () => void; +}) { + return ( +
+ {rows.map((row) => ( +
+
+ + onChange(rows.map((r) => (r.id === row.id ? { ...r, key: event.target.value } : r))) + } + /> +
+
+ + onChange( + rows.map((r) => (r.id === row.id ? { ...r, value: event.target.value } : r)) + ) + } + /> +
+ +
+ ))} + +
+ ); +} + +function setPath(target: Record, path: string, value: unknown) { + const parts = path.split("."); + let cursor = target; + for (let i = 0; i < parts.length - 1; i++) { + const key = parts[i]; + const next = cursor[key]; + if (typeof next !== "object" || next === null) { + cursor[key] = {}; + } + cursor = cursor[key] as Record; + } + cursor[parts[parts.length - 1]] = value; +} + +function buildHandshakeBody( + handshake: { matchPath: string; matchValue: string; respondPath: string }, + challenge: string +): Record { + const body: Record = {}; + setPath(body, handshake.matchPath, handshake.matchValue); + setPath(body, handshake.respondPath, challenge); + return body; +} + +function ConfirmSendDialog({ + environmentLabel, + disabled, + isSending, + onConfirm, +}: { + environmentLabel: string; + disabled: boolean; + isSending: boolean; + onConfirm: () => void; +}) { + const [open, setOpen] = useState(false); + return ( + + + + + + Send to {environmentLabel}? +
+

+ This delivers a real event to a non-development endpoint and triggers a real run. +

+
+ + +
+
+
+
+ ); +} + +function ResultStrip({ + result, + deliveryPath, +}: { + result: WebhookSendResult; + deliveryPath?: string; +}) { + const status = result.success ? result.httpStatus : undefined; + const handshake = result.success && result.handshake; + const deduplicated = result.success && result.deduplicated; + const ok = result.success && status === 200 && !deduplicated; + return ( +
+
+ + {handshake + ? "Handshake" + : deduplicated + ? "Deduplicated" + : result.success + ? `HTTP ${result.httpStatus}` + : "Failed"} + + {result.success && result.deliveryId ? ( + {result.deliveryId} + ) : null} + {deliveryPath ? ( + + {deduplicated ? "View original →" : "View delivery →"} + + ) : null} +
+ {handshake ? ( + + Challenge echoed by the endpoint. Handshakes are answered inline; no delivery is recorded. + + ) : deduplicated ? ( + + Identical payload was deduplicated to the original delivery. Vary it to send a new one. + + ) : null} + +
+ ); +} diff --git a/apps/webapp/app/components/webhookDeliveries/v1/DeliveriesTable.tsx b/apps/webapp/app/components/webhookDeliveries/v1/DeliveriesTable.tsx new file mode 100644 index 00000000000..253864aef35 --- /dev/null +++ b/apps/webapp/app/components/webhookDeliveries/v1/DeliveriesTable.tsx @@ -0,0 +1,260 @@ +import { ArrowRightIcon } from "@heroicons/react/20/solid"; +import { useLocation, useNavigation } from "@remix-run/react"; +import { AIChatIcon } from "~/assets/icons/AIChatIcon"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; +import { DateTime } from "~/components/primitives/DateTime"; +import { MiddleTruncate } from "~/components/primitives/MiddleTruncate"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { PopoverMenuItem } from "~/components/primitives/Popover"; +import { Spinner } from "~/components/primitives/Spinner"; +import { + Table, + TableBlankRow, + TableBody, + TableCell, + TableCellMenu, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { type WebhookDeliveryListItem } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { + v3RunPath, + v3SessionPath, + v3WebhookDeliveryPath, + v3WebhookTaskPath, +} from "~/utils/pathBuilder"; +import { cn } from "~/utils/cn"; +import { DeliveryStatusBadge } from "./DeliveryStatus"; + +export function DeliveriesTable({ + deliveries, + hasFilters, + showTopBorder = true, + stickyHeader = false, + showWebhook = false, +}: { + deliveries: WebhookDeliveryListItem[]; + hasFilters?: boolean; + showTopBorder?: boolean; + stickyHeader?: boolean; + // The top-level (cross-endpoint) deliveries page shows which webhook each delivery + // belongs to; the per-webhook detail page leaves this off. + showWebhook?: boolean; +}) { + const navigation = useNavigation(); + const location = useLocation(); + const isLoading = + navigation.state !== "idle" && navigation.location?.pathname === location.pathname; + + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + return ( + + + + {showWebhook && Webhook} + Delivery + Status + External delivery ID + Target + Created + Processed + Error + + Actions + + + + + {deliveries.length === 0 ? ( + +
+ + {hasFilters + ? "No deliveries match these filters" + : "No deliveries for this webhook yet"} + +
+
+ ) : ( + deliveries.map((delivery) => { + const runPath = delivery.run + ? v3RunPath(organization, project, environment, { + friendlyId: delivery.run.friendlyId, + }) + : undefined; + + // Session deliveries route to a session (their run is just its current turn), so the + // session is the meaningful target; task deliveries fall back to the run. + const sessionPath = delivery.session + ? v3SessionPath(organization, project, environment, { + friendlyId: delivery.session.friendlyId, + }) + : undefined; + + const webhookPath = delivery.webhook + ? v3WebhookTaskPath(organization, project, environment, delivery.webhook.slug) + : undefined; + + const deliveryPath = v3WebhookDeliveryPath( + organization, + project, + environment, + delivery.friendlyId + ); + + return ( + + {showWebhook && ( + + {delivery.webhook ? ( + + + {delivery.webhook.slug} + + ) : ( + Unknown + )} + + )} + + + {delivery.friendlyId} + {delivery.isTest ? ( + + Test + + ) : null} + + + + + + + {delivery.externalDeliveryId ? ( +
+ +
+ ) : ( + None + )} +
+ + {delivery.session ? ( + + + {delivery.session.friendlyId} + + ) : delivery.run ? ( + + + {delivery.run.friendlyId} + + ) : ( + None + )} + + + + + + {delivery.processedAt ? ( + + ) : ( + None + )} + + + {delivery.status === "FAILED" && delivery.errorMessage ? ( + + {delivery.errorMessage} + + } + /> + ) : ( + None + )} + + +
+ ); + }) + )} + {isLoading && ( + + Loading… + + )} +
+
+ ); +} + +function DeliveryActionsCell({ + deliveryPath, + runPath, + sessionPath, +}: { + deliveryPath: string; + runPath?: string; + sessionPath?: string; +}) { + return ( + + + {sessionPath ? ( + + ) : null} + {runPath ? ( + + ) : null} + + } + /> + ); +} diff --git a/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx b/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx new file mode 100644 index 00000000000..0fac38937a4 --- /dev/null +++ b/apps/webapp/app/components/webhookDeliveries/v1/DeliveryStatus.tsx @@ -0,0 +1,38 @@ +import { type WebhookDeliveryStatus } from "@trigger.dev/database"; +import { cn } from "~/utils/cn"; + +// Reuse the run-status hex palette for the four delivery statuses (matches the +// detail page activity chart and the task-list status bars). No invented colors. +export const DELIVERY_STATUS_COLOR: Record = { + SUCCEEDED: "#28BF5C", + FAILED: "#E11D48", + PROCESSING: "#3B82F6", + PENDING: "#878C99", + FILTERED: "#64748B", // received + verified, intentionally not routed; neutral, not a failure +}; + +export const DELIVERY_STATUS_LABEL: Record = { + SUCCEEDED: "Succeeded", + FAILED: "Failed", + PROCESSING: "Processing", + PENDING: "Pending", + FILTERED: "Filtered", +}; + +export function DeliveryStatusBadge({ + status, + className, +}: { + status: WebhookDeliveryStatus; + className?: string; +}) { + return ( + + + {DELIVERY_STATUS_LABEL[status]} + + ); +} diff --git a/apps/webapp/app/components/webhookDeliveries/v1/DeliveryTimeline.tsx b/apps/webapp/app/components/webhookDeliveries/v1/DeliveryTimeline.tsx new file mode 100644 index 00000000000..9f7e1a4cd9e --- /dev/null +++ b/apps/webapp/app/components/webhookDeliveries/v1/DeliveryTimeline.tsx @@ -0,0 +1,111 @@ +import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; +import { Fragment } from "react"; +import { AIChatIcon } from "~/assets/icons/AIChatIcon"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; +import { DateTimeAccurate } from "~/components/primitives/DateTime"; +import { TextLink } from "~/components/primitives/TextLink"; +import { RunTimelineEvent, RunTimelineLine } from "~/components/run/RunTimeline"; +import { LiveTimer } from "~/components/runs/v3/LiveTimer"; +import { + buildDeliveryTimelineItems, + type BuildDeliveryTimelineInput, + type DeliveryTimelineEventItem, +} from "./buildDeliveryTimelineItems"; + +type DeliveryTimelineProps = { + delivery: BuildDeliveryTimelineInput; + runPath?: string; + sessionPath?: string; +}; + +export function DeliveryTimeline({ delivery, runPath, sessionPath }: DeliveryTimelineProps) { + const items = buildDeliveryTimelineItems(delivery); + + return ( +
+ {items.map((item) => { + if (item.type === "line") { + return ( + + {item.to ? ( + formatDuration(item.from, item.to) + ) : ( + + )} + {item.label ? ( + ({item.label.toLowerCase()}) + ) : null} + + } + /> + ); + } + + return ( + + + ) : null + } + /> + + + ); + })} +
+ ); +} + +function TimelineEventExtras({ + item, + runPath, + sessionPath, +}: { + item: DeliveryTimelineEventItem; + runPath?: string; + sessionPath?: string; +}) { + const showSession = Boolean(item.target?.session && sessionPath); + const showRun = Boolean(item.target?.run && runPath); + const hasTarget = showSession || showRun; + + if (!item.note && !hasTarget) { + return null; + } + + return ( +
+
+
+ {item.note ? ( + + {item.note} + + ) : null} + {showSession && item.target?.session ? ( + + + {item.target.session.friendlyId} + + ) : showRun && item.target?.run ? ( + + + {item.target.run.friendlyId} + + ) : null} +
+
+ ); +} diff --git a/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx b/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx new file mode 100644 index 00000000000..33f91662dc5 --- /dev/null +++ b/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx @@ -0,0 +1,660 @@ +import * as Ariakit from "@ariakit/react"; +import { BeakerIcon, FingerPrintIcon, PlusIcon, XMarkIcon } from "@heroicons/react/20/solid"; +import { Form } from "@remix-run/react"; +import { type WebhookDeliveryStatus } from "@trigger.dev/database"; +import { type ReactNode, useMemo, useRef, useState } from "react"; +import { StatusIcon } from "~/assets/icons/StatusIcon"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; +import { AppliedFilter } from "~/components/primitives/AppliedFilter"; +import { + ComboBox, + SelectButtonItem, + SelectItem, + SelectList, + SelectPopover, + SelectProvider, + SelectTrigger, + shortcutFromIndex, +} from "~/components/primitives/Select"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/primitives/Tooltip"; +import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; +import { useSearchParams } from "~/hooks/useSearchParam"; +import { useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { Button } from "../../primitives/Buttons"; +import { + appliedSummary, + FilterMenuProvider, + IdFilterDropdown, + type IdFilterDropdownProps, + TimeFilter, +} from "~/components/runs/v3/SharedFilters"; + +// Match DeliveriesTable's DELIVERY_STATUS_COLOR / DELIVERY_STATUS_LABEL. +const deliveryStatuses: { value: WebhookDeliveryStatus; title: string; color: string }[] = [ + { value: "PENDING", title: "Pending", color: "#878C99" }, + { value: "PROCESSING", title: "Processing", color: "#3B82F6" }, + { value: "SUCCEEDED", title: "Succeeded", color: "#28BF5C" }, + { value: "FAILED", title: "Failed", color: "#E11D48" }, +]; + +const statusTitleByValue = new Map(deliveryStatuses.map((s) => [s.value, s.title])); + +function StatusDot({ color }: { color: string }) { + return ; +} + +export type PossibleWebhook = { slug: string; source: string }; + +type WebhookDeliveryFiltersProps = { + possibleWebhooks: PossibleWebhook[]; + /** Custom default period for the time filter (e.g., "1h", "7d") */ + defaultPeriod?: string; +}; + +export function WebhookDeliveryFilters(props: WebhookDeliveryFiltersProps) { + const location = useOptimisticLocation(); + const searchParams = new URLSearchParams(location.search); + const hasFilters = + searchParams.has("statuses") || + searchParams.has("webhooks") || + searchParams.has("deliveryId") || + searchParams.has("runId") || + searchParams.has("test"); + + return ( +
+ + + + + + + {hasFilters && ( +
+
+ ); +} + +const filterTypes = [ + { name: "deliveryId", title: "Delivery ID", icon: }, + { name: "runId", title: "Run ID", icon: }, +] as const; + +type FilterType = (typeof filterTypes)[number]["name"]; + +const moreFiltersShortcut = { key: "f" }; + +function FilterMenu() { + const [filterType, setFilterType] = useState(); + + const filterTrigger = ( + + +
+ } + variant={"secondary/small"} + shortcut={moreFiltersShortcut} + tooltipTitle={"More filters"} + className="pl-1 pr-2" + > + More filters + + ); + + return ( + setFilterType(undefined)}> + {(search, setSearch) => ( + setSearch("")} + trigger={filterTrigger} + filterType={filterType} + setFilterType={setFilterType} + /> + )} + + ); +} + +function AppliedFilters() { + return ( + <> + + + + ); +} + +type MenuProps = { + searchValue: string; + clearSearchValue: () => void; + trigger: ReactNode; + filterType: FilterType | undefined; + setFilterType: (filterType: FilterType | undefined) => void; +}; + +function Menu(props: MenuProps) { + switch (props.filterType) { + case undefined: + return ; + case "deliveryId": + return props.setFilterType(undefined)} {...props} />; + case "runId": + return props.setFilterType(undefined)} {...props} />; + } +} + +function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) { + const filtered = useMemo(() => { + return filterTypes.filter((item) => + item.title.toLowerCase().includes(searchValue.toLowerCase()) + ); + }, [searchValue]); + + return ( + + {trigger} + + + + {filtered.map((type, index) => ( + { + clearSearchValue(); + setFilterType(type.name); + }} + icon={type.icon} + shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })} + > + {type.title} + + ))} + + + + ); +} + +function StatusDropdown({ + trigger, + clearSearchValue, + searchValue, + onClose, +}: { + trigger: ReactNode; + clearSearchValue: () => void; + searchValue: string; + onClose?: () => void; +}) { + const { values, replace } = useSearchParams(); + + const handleChange = (values: string[]) => { + clearSearchValue(); + replace({ statuses: values, cursor: undefined, direction: undefined }); + }; + + const filtered = useMemo(() => { + return deliveryStatuses.filter((item) => + item.title.toLowerCase().includes(searchValue.toLowerCase()) + ); + }, [searchValue]); + + return ( + + {trigger} + { + if (onClose) { + onClose(); + return false; + } + + return true; + }} + > + + + {filtered.map((item, index) => ( + + + + {item.title} + + + ))} + + + + ); +} + +const statusShortcut = { key: "s" }; + +function PermanentStatusFilter() { + const { values, del } = useSearchParams(); + const statuses = values("statuses"); + const hasStatuses = statuses.length > 0 && !statuses.every((v) => v === ""); + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: statusShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); + + return ( + + {(search, setSearch) => ( + + } + /> + } + > + {hasStatuses ? ( + } + value={appliedSummary( + statuses.map((v) => statusTitleByValue.get(v as WebhookDeliveryStatus) ?? v) + )} + onRemove={() => del(["statuses", "cursor", "direction"])} + variant="secondary/small" + className="pl-1" + /> + ) : ( +
+
+
+
+ Status +
+ )} + + +
+ Filter by status + +
+
+ + } + searchValue={search} + clearSearchValue={() => setSearch("")} + /> + )} + + ); +} + +function WebhookDropdown({ + trigger, + clearSearchValue, + searchValue, + onClose, + possibleWebhooks, +}: { + trigger: ReactNode; + clearSearchValue: () => void; + searchValue: string; + onClose?: () => void; + possibleWebhooks: PossibleWebhook[]; +}) { + const { values, replace } = useSearchParams(); + + const handleChange = (newValues: string[]) => { + clearSearchValue(); + replace({ + webhooks: newValues.length > 0 ? newValues : undefined, + cursor: undefined, + direction: undefined, + }); + }; + + const filtered = useMemo(() => { + return possibleWebhooks.filter((item) => + item.slug.toLowerCase().includes(searchValue.toLowerCase()) + ); + }, [searchValue, possibleWebhooks]); + + return ( + + {trigger} + { + if (onClose) { + onClose(); + return false; + } + + return true; + }} + > + + + {filtered.length > 0 ? ( + filtered.map((item) => ( + } + className="text-text-bright" + > + {item.slug} + + )) + ) : ( + No webhooks found + )} + + + + ); +} + +const webhookShortcut = { key: "w" }; + +function PermanentWebhookFilter({ possibleWebhooks }: { possibleWebhooks: PossibleWebhook[] }) { + const { values, del } = useSearchParams(); + const webhooks = values("webhooks"); + const hasWebhooks = webhooks.length > 0 && !webhooks.every((v) => v === ""); + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: webhookShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); + + return ( + + {(search, setSearch) => ( + + } + /> + } + > + {hasWebhooks ? ( + } + value={appliedSummary(webhooks)} + onRemove={() => del(["webhooks", "cursor", "direction"])} + variant="secondary/small" + className="pl-1" + /> + ) : ( +
+ + Webhook +
+ )} +
+ +
+ Filter by webhook + +
+
+ + } + searchValue={search} + clearSearchValue={() => setSearch("")} + possibleWebhooks={possibleWebhooks} + /> + )} +
+ ); +} + +const testModes = [ + { value: "all", title: "All deliveries" }, + { value: "hide", title: "Hide test sends" }, + { value: "only", title: "Test sends only" }, +] as const; + +const testShortcut = { key: "t" }; + +function TestDropdown({ trigger, onClose }: { trigger: ReactNode; onClose?: () => void }) { + const { value, replace } = useSearchParams(); + const current = value("test") ?? "all"; + + const handleChange = (next: string) => { + replace({ test: next === "all" ? undefined : next, cursor: undefined, direction: undefined }); + }; + + return ( + + {trigger} + { + if (onClose) { + onClose(); + return false; + } + return true; + }} + > + + {testModes.map((mode, index) => ( + + {mode.title} + + ))} + + + + ); +} + +function PermanentTestFilter() { + const { value, del } = useSearchParams(); + const current = value("test"); + const active = current === "hide" || current === "only"; + const triggerRef = useRef(null); + + useShortcutKeys({ + shortcut: testShortcut, + action: (e) => { + e.preventDefault(); + e.stopPropagation(); + triggerRef.current?.click(); + }, + }); + + return ( + + } + /> + } + > + {active ? ( + } + value={current === "only" ? "Test sends only" : "Hidden"} + onRemove={() => del(["test", "cursor", "direction"])} + variant="secondary/small" + className="pl-1" + /> + ) : ( +
+ + Test +
+ )} +
+ +
+ Filter test sends + +
+
+ + } + /> + ); +} + +function DeliveryIdDropdown( + props: Omit +) { + return ( + + ); +} + +function AppliedDeliveryIdFilter() { + const { value, del } = useSearchParams(); + + if (value("deliveryId") === undefined) { + return null; + } + + const deliveryId = value("deliveryId"); + + return ( + + {(search, setSearch) => ( + }> + } + value={deliveryId} + onRemove={() => del(["deliveryId", "cursor", "direction"])} + variant="secondary/small" + /> + + } + searchValue={search} + clearSearchValue={() => setSearch("")} + /> + )} + + ); +} + +function validateRunId(value: string): string | undefined { + if (!value.startsWith("run_")) return "Run IDs start with 'run_'"; + if (value.length !== 25 && value.length !== 29) return "Run IDs are 25 or 29 characters long"; +} + +function RunIdDropdown( + props: Omit< + IdFilterDropdownProps, + "label" | "placeholder" | "paramKey" | "validate" | "inputWidth" + > +) { + return ( + + ); +} + +function AppliedRunIdFilter() { + const { value, del } = useSearchParams(); + + if (value("runId") === undefined) { + return null; + } + + const runId = value("runId"); + + return ( + + {(search, setSearch) => ( + }> + } + value={runId} + onRemove={() => del(["runId", "cursor", "direction"])} + variant="secondary/small" + /> + + } + searchValue={search} + clearSearchValue={() => setSearch("")} + /> + )} + + ); +} diff --git a/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts b/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts new file mode 100644 index 00000000000..f5cc0af596a --- /dev/null +++ b/apps/webapp/app/components/webhookDeliveries/v1/buildDeliveryTimelineItems.ts @@ -0,0 +1,125 @@ +import { type WebhookDeliveryStatus } from "@trigger.dev/database"; +import { + type TimelineEventState, + type TimelineEventVariant, + type TimelineLineVariant, +} from "~/components/run/RunTimeline"; + +export type DeliveryRunTarget = { + run: { friendlyId: string } | null; + session: { friendlyId: string; externalId: string | null } | null; +}; + +export type DeliveryTimelineEventItem = { + type: "event"; + id: string; + title: string; + date: Date | null; + previousDate?: Date; + state: TimelineEventState; + variant: TimelineEventVariant; + note?: string | null; + target?: DeliveryRunTarget; +}; + +export type DeliveryTimelineLineItem = { + type: "line"; + id: string; + from: Date; + to: Date | null; + state: TimelineEventState; + variant: TimelineLineVariant; + label?: string; +}; + +export type DeliveryTimelineItem = DeliveryTimelineEventItem | DeliveryTimelineLineItem; + +export type BuildDeliveryTimelineInput = { + status: WebhookDeliveryStatus; + createdAt: Date; + processedAt: Date | null; + errorMessage: string | null; + filterReason: string | null; + run: { friendlyId: string } | null; + session: { friendlyId: string; externalId: string | null } | null; +}; + +export function buildDeliveryTimelineItems( + delivery: BuildDeliveryTimelineInput +): DeliveryTimelineItem[] { + const { status, createdAt, processedAt } = delivery; + const inFlight = status === "PENDING" || status === "PROCESSING"; + + const items: DeliveryTimelineItem[] = [ + { + type: "event", + id: "received", + title: "Received", + date: createdAt, + state: "complete", + variant: "start-cap", + }, + ]; + + if (status === "FILTERED") { + items.push({ + type: "line", + id: "routing", + from: createdAt, + to: processedAt ?? createdAt, + state: "delayed", + variant: "light", + }); + items.push({ + type: "event", + id: "filtered", + title: "Filtered", + date: processedAt ?? createdAt, + previousDate: createdAt, + state: "delayed", + variant: "dot-solid", + note: delivery.filterReason, + }); + return items; + } + + items.push({ + type: "line", + id: "routing", + from: createdAt, + to: inFlight ? null : processedAt, + state: inFlight ? "inprogress" : status === "FAILED" ? "error" : "complete", + variant: "normal", + label: "Routing", + }); + + if (inFlight) { + return items; + } + + if (status === "SUCCEEDED") { + items.push({ + type: "event", + id: "delivered", + title: "Delivered", + date: processedAt, + previousDate: createdAt, + state: "complete", + variant: "end-cap-thick", + target: { run: delivery.run, session: delivery.session }, + }); + } else if (status === "FAILED") { + items.push({ + type: "event", + id: "failed", + title: "Failed", + date: processedAt, + previousDate: createdAt, + state: "error", + variant: "end-cap-thick", + note: delivery.errorMessage, + }); + } + + return items; +} diff --git a/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts b/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts new file mode 100644 index 00000000000..66e97fa1804 --- /dev/null +++ b/apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts @@ -0,0 +1,237 @@ +import { useLocation } from "@remix-run/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTypedFetcher } from "remix-typedjson"; +import { useInterval } from "~/hooks/useInterval"; +import { type WebhookDeliveryListItem } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { + type LiveDeliveryFields, + type loader as liveDeliveriesLoader, +} from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live"; + +const DELIVERIES_POLL_INTERVAL_MS = 3000; +const NEW_DELIVERIES_EVERY_N_POLL_TICKS = 2; + +const IN_FLIGHT_STATUSES = new Set(["PENDING", "PROCESSING"]); + +type ListedDelivery = WebhookDeliveryListItem; +type LivePollFetcherData = + | { deliveries: LiveDeliveryFields[] } + | { deliveries: LiveDeliveryFields[]; count: number; since: number } + | undefined; + +function hasNewDeliveriesCountFields( + data: LivePollFetcherData +): data is NonNullable & { count: number; since: number } { + return data !== undefined && "count" in data && "since" in data; +} + +function maxCreatedAtMs(deliveries: ListedDelivery[]): number | undefined { + if (deliveries.length === 0) return undefined; + + return deliveries.reduce((maxTimestamp, delivery) => { + return Math.max(maxTimestamp, new Date(delivery.createdAt).getTime()); + }, 0); +} + +function patchVisibleDeliveriesWithLiveUpdates( + currentDeliveries: ListedDelivery[], + liveDeliveries: LiveDeliveryFields[] +) { + const updatesById = new Map(liveDeliveries.map((delivery) => [delivery.friendlyId, delivery])); + + return currentDeliveries.map((delivery) => { + const update = updatesById.get(delivery.friendlyId); + if (!update) return delivery; + + return { + ...delivery, + status: update.status, + runId: update.runId, + run: update.run, + session: update.session, + errorMessage: update.errorMessage, + processedAt: update.processedAt, + }; + }); +} + +function isNewDeliveriesCheckTick(tick: number) { + return tick === 1 || tick % NEW_DELIVERIES_EVERY_N_POLL_TICKS === 0; +} + +function useNewDeliveriesDetection({ + deliveries, + hasAnyDeliveries, + isLoading, +}: { + deliveries: ListedDelivery[]; + hasAnyDeliveries: boolean; + isLoading: boolean; +}) { + const pollTickRef = useRef(0); + const [knownNewestDeliveryMs, setKnownNewestDeliveryMs] = useState( + () => maxCreatedAtMs(deliveries) ?? Date.now() + ); + const [newDeliveriesCount, setNewDeliveriesCount] = useState(0); + + const shouldPollForNewDeliveries = hasAnyDeliveries && !isLoading && newDeliveriesCount < 100; + + const resetNewDeliveriesTracking = useCallback(() => { + setKnownNewestDeliveryMs(maxCreatedAtMs(deliveries) ?? Date.now()); + setNewDeliveriesCount(0); + pollTickRef.current = 0; + }, [deliveries]); + + const dismissNewDeliveries = useCallback(() => { + setNewDeliveriesCount(0); + setKnownNewestDeliveryMs(Date.now()); + pollTickRef.current = 0; + }, []); + + const checkNewDeliveriesOnTick = useCallback(() => { + pollTickRef.current += 1; + return shouldPollForNewDeliveries && isNewDeliveriesCheckTick(pollTickRef.current); + }, [shouldPollForNewDeliveries]); + + const showNewDeliveriesBanner = newDeliveriesCount > 0; + + return { + knownNewestDeliveryMs, + newDeliveriesCount, + setNewDeliveriesCount, + shouldPollForNewDeliveries, + showNewDeliveriesBanner, + dismissNewDeliveries, + checkNewDeliveriesOnTick, + resetNewDeliveriesTracking, + }; +} + +export function useDeliveriesLiveReload({ + deliveries, + hasAnyDeliveries, + isLoading, + webhookEndpointId, + organizationSlug, + projectSlug, + environmentSlug, +}: { + deliveries: ListedDelivery[]; + hasAnyDeliveries: boolean; + isLoading: boolean; + /** Omit to poll across every endpoint in the environment (the cross-endpoint deliveries list). */ + webhookEndpointId?: string; + organizationSlug: string; + projectSlug: string; + environmentSlug: string; +}) { + const location = useLocation(); + const deliveriesPollFetcher = useTypedFetcher(); + const deliveriesPollFetcherStateRef = useRef(deliveriesPollFetcher.state); + deliveriesPollFetcherStateRef.current = deliveriesPollFetcher.state; + + const [visibleDeliveries, setVisibleDeliveries] = useState(deliveries); + + const { + knownNewestDeliveryMs, + newDeliveriesCount, + setNewDeliveriesCount, + shouldPollForNewDeliveries, + showNewDeliveriesBanner, + dismissNewDeliveries, + checkNewDeliveriesOnTick, + resetNewDeliveriesTracking, + } = useNewDeliveriesDetection({ deliveries, hasAnyDeliveries, isLoading }); + + useEffect(() => { + setVisibleDeliveries(deliveries); + resetNewDeliveriesTracking(); + }, [deliveries, location.search, resetNewDeliveriesTracking]); + + useEffect(() => { + const data = deliveriesPollFetcher.data; + if (!data?.deliveries.length) return; + + setVisibleDeliveries((current) => + patchVisibleDeliveriesWithLiveUpdates(current, data.deliveries) + ); + }, [deliveriesPollFetcher.data]); + + useEffect(() => { + const data = deliveriesPollFetcher.data; + if (!hasNewDeliveriesCountFields(data)) return; + + if (data.since === knownNewestDeliveryMs) { + setNewDeliveriesCount(data.count); + } + }, [deliveriesPollFetcher.data, knownNewestDeliveryMs, setNewDeliveriesCount]); + + const activeDeliveryIdsParam = useMemo( + () => + visibleDeliveries + .filter((delivery) => IN_FLIGHT_STATUSES.has(delivery.status)) + .map((delivery) => delivery.friendlyId) + .join(","), + [visibleDeliveries] + ); + const hasActiveDeliveries = activeDeliveryIdsParam.length > 0; + + const deliveriesResourcesBasePath = useMemo( + () => + `/resources/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/webhooks/deliveries`, + [organizationSlug, projectSlug, environmentSlug] + ); + + const loadDeliveriesPoll = useCallback( + (checkForNewDeliveries: boolean) => { + if (deliveriesPollFetcherStateRef.current !== "idle") return; + + if (!hasActiveDeliveries && !checkForNewDeliveries) return; + + const searchParams = new URLSearchParams(); + if (webhookEndpointId) { + searchParams.set("webhookEndpointId", webhookEndpointId); + } + if (hasActiveDeliveries) { + searchParams.set("deliveryIds", activeDeliveryIdsParam); + } + + if (checkForNewDeliveries) { + searchParams.set("includeNewDeliveries", "true"); + searchParams.set("since", String(knownNewestDeliveryMs)); + const to = new URLSearchParams(location.search).get("to"); + if (to) searchParams.set("to", to); + } + + deliveriesPollFetcher.load(`${deliveriesResourcesBasePath}/live?${searchParams.toString()}`); + }, + [ + activeDeliveryIdsParam, + hasActiveDeliveries, + location.search, + knownNewestDeliveryMs, + webhookEndpointId, + deliveriesPollFetcher, + deliveriesResourcesBasePath, + ] + ); + + const shouldPoll = !isLoading && (hasActiveDeliveries || shouldPollForNewDeliveries); + + useInterval({ + interval: DELIVERIES_POLL_INTERVAL_MS, + onLoad: true, + pauseWhenHidden: true, + disabled: !shouldPoll, + callback: () => { + loadDeliveriesPoll(checkNewDeliveriesOnTick()); + }, + }); + + return { + visibleDeliveries, + showNewDeliveriesBanner, + newDeliveriesCount, + dismissNewDeliveries, + }; +} diff --git a/apps/webapp/app/components/webhookEndpoints/v1/EndpointStatus.tsx b/apps/webapp/app/components/webhookEndpoints/v1/EndpointStatus.tsx new file mode 100644 index 00000000000..277950968ac --- /dev/null +++ b/apps/webapp/app/components/webhookEndpoints/v1/EndpointStatus.tsx @@ -0,0 +1,25 @@ +import { type WebhookEndpointStatus } from "@trigger.dev/database"; + +const ENDPOINT_STATUS_COLOR: Record = { + ACTIVE: "#28BF5C", + INACTIVE: "#878C99", + DELETING: "#F59E0B", +}; + +const ENDPOINT_STATUS_LABEL: Record = { + ACTIVE: "Active", + INACTIVE: "Inactive", + DELETING: "Deleting", +}; + +export function EndpointStatusBadge({ status }: { status: WebhookEndpointStatus }) { + return ( + + + {ENDPOINT_STATUS_LABEL[status]} + + ); +} diff --git a/apps/webapp/app/components/webhookEndpoints/v1/EndpointsTable.tsx b/apps/webapp/app/components/webhookEndpoints/v1/EndpointsTable.tsx new file mode 100644 index 00000000000..556a64fa744 --- /dev/null +++ b/apps/webapp/app/components/webhookEndpoints/v1/EndpointsTable.tsx @@ -0,0 +1,117 @@ +import { ArrowRightIcon } from "@heroicons/react/20/solid"; +import { Badge } from "~/components/primitives/Badge"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import { PopoverMenuItem } from "~/components/primitives/Popover"; +import { + Table, + TableBlankRow, + TableBody, + TableCell, + TableCellMenu, + TableHeader, + TableHeaderCell, + TableRow, +} from "~/components/primitives/Table"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { type WebhookEndpointListItem } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { v3WebhookEndpointPath } from "~/utils/pathBuilder"; +import { EndpointStatusBadge } from "./EndpointStatus"; + +export function EndpointsTable({ + endpoints, + stickyHeader = false, + showTopBorder = true, +}: { + endpoints: WebhookEndpointListItem[]; + stickyHeader?: boolean; + showTopBorder?: boolean; +}) { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + return ( + + + + Endpoint + Tenant + External ref + Status + Secret + Deliveries (7d) + + Actions + + + + + {endpoints.length === 0 ? ( + +
+ No endpoints for this webhook yet +
+
+ ) : ( + endpoints.map((endpoint) => { + const endpointPath = v3WebhookEndpointPath( + organization, + project, + environment, + endpoint.friendlyId + ); + + return ( + + + + {endpoint.friendlyId} + {endpoint.isDefault ? default : null} + + + + {endpoint.tenantId ?? default} + + + {endpoint.externalRef ?? None} + + + + + + {endpoint.hasSigningSecret ? ( + + + Set + + ) : ( + + + Not set + + )} + + + {endpoint.deliveryCount.toLocaleString()} + + + } + /> + + ); + }) + )} +
+
+ ); +} diff --git a/apps/webapp/app/db.server.ts b/apps/webapp/app/db.server.ts index 97a3ae7bb53..1cec4fcc219 100644 --- a/apps/webapp/app/db.server.ts +++ b/apps/webapp/app/db.server.ts @@ -6,6 +6,8 @@ import { type PrismaReplicaClient, type PrismaTransactionClient, type PrismaTransactionOptions, + type WebhookDatabase, + type WebhookReplicaDatabase, } from "@trigger.dev/database"; import { RunOpsPrismaClient } from "@internal/run-ops-database"; import { markReadReplicaClient } from "@internal/run-store"; @@ -36,6 +38,8 @@ export type { PrismaClientOrTransaction, PrismaTransactionOptions, PrismaReplicaClient, + WebhookDatabase, + WebhookReplicaDatabase, }; // Boundary logger for transac(): skips an error the client extension already @@ -179,6 +183,52 @@ export const $replica: PrismaReplicaClient = singleton("replica", () => { : prisma; }); +/** + * Webhook feature data-plane seam. The whole webhook feature (WebhookEndpoint + WebhookDelivery) + * can run on a dedicated Postgres via WEBHOOK_DATABASE_URL; unset reuses the main prisma instance, + * so single-DB installs open no extra pool. + */ +export const webhookPrisma: WebhookDatabase = singleton("webhookPrisma", () => { + if (!env.WEBHOOK_DATABASE_URL) { + return prisma; + } + return captureInfrastructureErrors( + tagDatasource( + "writer", + buildWriterClient({ + url: env.WEBHOOK_DATABASE_URL, + clientType: "webhook-writer", + connectionLimit: env.WEBHOOK_DATABASE_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT, + }) + ) + ); +}); + +/** + * Webhook reader chain: an explicit webhook replica, else the webhook writer once split (no + * separate replica yet), else the main $replica when the feature is not split. + */ +export const webhookReplica: WebhookReplicaDatabase = singleton("webhookReplica", () => { + if (env.WEBHOOK_DATABASE_READ_REPLICA_URL) { + return markReadReplicaClient( + captureInfrastructureErrors( + tagDatasource( + "replica", + buildReplicaClient({ + url: env.WEBHOOK_DATABASE_READ_REPLICA_URL, + clientType: "webhook-reader", + connectionLimit: env.WEBHOOK_DATABASE_CONNECTION_LIMIT ?? env.DATABASE_CONNECTION_LIMIT, + }) + ) + ) + ); + } + if (env.WEBHOOK_DATABASE_URL) { + return webhookPrisma; + } + return $replica; +}); + export type RunOpsClients = { writer: PrismaClient; replica: PrismaReplicaClient }; export type NewRunOpsClients = { writer: RunOpsPrismaClient; replica: RunOpsPrismaClient }; export type RunOpsTopology = { @@ -391,12 +441,14 @@ function getClient() { export function buildWriterClient({ url, clientType, + connectionLimit = env.DATABASE_CONNECTION_LIMIT, }: { url: string; clientType: string; + connectionLimit?: number; }): PrismaClient { const databaseUrl = extendQueryParams(url, { - connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(), + connection_limit: connectionLimit.toString(), pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(), connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(), application_name: env.SERVICE_NAME, @@ -538,12 +590,14 @@ function getReplicaClient() { export function buildReplicaClient({ url, clientType, + connectionLimit = env.DATABASE_CONNECTION_LIMIT, }: { url: string; clientType: string; + connectionLimit?: number; }): PrismaClient { const replicaUrl = extendQueryParams(url, { - connection_limit: env.DATABASE_CONNECTION_LIMIT.toString(), + connection_limit: connectionLimit.toString(), pool_timeout: env.DATABASE_POOL_TIMEOUT.toString(), connection_timeout: env.DATABASE_CONNECTION_TIMEOUT.toString(), application_name: env.SERVICE_NAME, diff --git a/apps/webapp/app/entry.server.tsx b/apps/webapp/app/entry.server.tsx index da1ab5a433a..fe2f9d34b83 100644 --- a/apps/webapp/app/entry.server.tsx +++ b/apps/webapp/app/entry.server.tsx @@ -39,6 +39,18 @@ import { registerRunChangeNotifierHandlers } from "./services/realtime/runChange // TRI-9864 for the incident write-up. import { sessionsReplicationInstance } from "./services/sessionsReplicationInstance.server"; (globalThis as Record).__sessionsReplicationInstance = sessionsReplicationInstance; +// Touch the webhook deliveries replication singleton at entry so it boots +// deterministically alongside the sessions replicator. Same `sideEffects: false` +// tree-shaking constraint applies — assign to globalThis, do NOT use `void`. +import { webhookDeliveriesReplicationInstance } from "./services/webhookDeliveriesReplicationInstance.server"; +(globalThis as Record).__webhookDeliveriesReplicationInstance = + webhookDeliveriesReplicationInstance; +// Touch the webhook engine singleton at entry so its redis-worker boots +// deterministically on webapp startup (the constructor calls worker.start()). +// Same `sideEffects: false` tree-shaking constraint applies: assign to +// globalThis, do NOT use `void`. +import { webhookEngine } from "./v3/webhookEngine.server"; +(globalThis as Record).__webhookEngine = webhookEngine; import { globalFlagsRegistry } from "./v3/globalFlagsRegistry.server"; (globalThis as Record).__globalFlagsRegistry = globalFlagsRegistry; import { workerRegionRegistry } from "./v3/workerRegions.server"; @@ -303,6 +315,7 @@ export { engineRateLimiter } from "./services/engineRateLimit.server"; export { otlpRateLimiter } from "./services/otlpRateLimit.server"; export { runWithHttpContext } from "./services/httpAsyncStorage.server"; export { tenantContextMiddleware } from "./services/tenantContextResolver.server"; +export { webhookIngressIpRateLimiter } from "./services/webhookIngressIpRateLimit.server"; export { socketIo } from "./v3/handleSocketIo.server"; export { wss } from "./v3/handleWebsockets.server"; diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 53b5edf0a46..76bdf5eacf5 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -211,6 +211,11 @@ const EnvironmentSchema = z // Control-plane cache relax knobs. Unset -> defaults (DEFAULT_CP_CACHE_TTL_MS / _MAX_ENTRIES). CONTROL_PLANE_CACHE_TTL_MS: z.coerce.number().int().optional(), CONTROL_PLANE_CACHE_MAX_ENTRIES: z.coerce.number().int().optional(), + // Webhook feature data-plane DB (WebhookEndpoint + WebhookDelivery). Unset -> the webhook + // clients reuse the main prisma / $replica, so this is connection-neutral until you split. + WEBHOOK_DATABASE_URL: z.string().optional(), + WEBHOOK_DATABASE_READ_REPLICA_URL: z.string().optional(), + WEBHOOK_DATABASE_CONNECTION_LIMIT: z.coerce.number().int().optional(), SESSION_SECRET: z.string().min(1).refine(isNotInsecureSecret, INSECURE_SECRET_MESSAGE), MAGIC_LINK_SECRET: z.string().min(1).refine(isNotInsecureSecret, INSECURE_SECRET_MESSAGE), ENCRYPTION_KEY: z @@ -1615,6 +1620,64 @@ const EnvironmentSchema = z .default(process.env.REDIS_TLS_DISABLED ?? "false"), SCHEDULE_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), + WEBHOOK_ENGINE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"), + WEBHOOK_WORKER_ENABLED: z.string().default(process.env.WORKER_ENABLED ?? "true"), + WEBHOOK_WORKER_CONCURRENCY_LIMIT: z.coerce.number().int().default(50), + WEBHOOK_WORKER_CONCURRENCY_WORKERS: z.coerce.number().int().default(2), + WEBHOOK_WORKER_CONCURRENCY_TASKS_PER_WORKER: z.coerce.number().int().default(10), + WEBHOOK_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000), + WEBHOOK_WORKER_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().default(30_000), + + WEBHOOK_WORKER_REDIS_HOST: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_HOST), + WEBHOOK_WORKER_REDIS_PORT: z.coerce + .number() + .optional() + .transform( + (v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined) + ), + WEBHOOK_WORKER_REDIS_USERNAME: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_USERNAME), + WEBHOOK_WORKER_REDIS_PASSWORD: z + .string() + .optional() + .transform((v) => v ?? process.env.REDIS_PASSWORD), + WEBHOOK_WORKER_REDIS_TLS_DISABLED: z + .string() + .default(process.env.REDIS_TLS_DISABLED ?? "false"), + + WEBHOOK_PARTITION_ENSURE_SCHEDULE: z.string().optional(), + WEBHOOK_PARTITION_ENSURE_JITTER_MS: z.coerce.number().int().optional(), + WEBHOOK_PARTITION_LOOKAHEAD_DAYS: z.coerce.number().int().default(10), + WEBHOOK_PARTITION_RETENTION_DAYS: z.coerce.number().int().default(60), + + // Ingest hot-path cache for the endpoint + resolved signing secret (keyed by opaqueId). 0 disables. + WEBHOOK_ENDPOINT_CACHE_TTL_MS: z.coerce.number().int().default(30_000), + WEBHOOK_ENDPOINT_CACHE_MAX_SIZE: z.coerce.number().int().default(10_000), + + WEBHOOK_INGRESS_ENABLED: z.string().default("1"), + + // Public origin for the webhook ingress URL shown to users / returned by the API. Defaults to the + // API origin; set to a dedicated host (e.g. https://webhook.trigger.dev) when one is fronted. + WEBHOOK_INGRESS_ORIGIN: z.string().optional(), + WEBHOOK_INGRESS_BODY_SIZE_LIMIT_MB: z.coerce.number().int().default(1), + WEBHOOK_INGRESS_RATE_LIMIT_WINDOW: z.string().default("10s"), + WEBHOOK_INGRESS_RATE_LIMIT_TOKENS: z.coerce.number().int().default(100), + WEBHOOK_INGRESS_IP_RATE_LIMIT_WINDOW: z.string().default("10s"), + WEBHOOK_INGRESS_IP_RATE_LIMIT_TOKENS: z.coerce.number().int().default(300), + WEBHOOK_FRONT_GATE_DEFAULT_TTL_SECONDS: z.coerce + .number() + .int() + .default(6 * 60 * 60), + WEBHOOK_FRONT_GATE_MAX_TTL_SECONDS: z.coerce + .number() + .int() + .default(6 * 60 * 60), + TASK_EVENT_PARTITIONING_ENABLED: z.string().default("0"), TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS: z.coerce.number().int().default(60), // 1 minute @@ -1770,6 +1833,47 @@ const EnvironmentSchema = z SESSION_REPLICATION_INSERT_BASE_DELAY_MS: z.coerce.number().int().default(100), SESSION_REPLICATION_INSERT_MAX_DELAY_MS: z.coerce.number().int().default(2000), + // Webhook deliveries replication (Postgres → ClickHouse webhook_deliveries_v1). + // Shares Redis with the runs replicator for leader locking but has its own + // slot and publication so the two consume independently. The source table is + // a partitioned parent, so the publication is created with + // publish_via_partition_root. + WEBHOOK_DELIVERIES_REPLICATION_CLICKHOUSE_URL: z.string().optional(), + WEBHOOK_DELIVERIES_REPLICATION_ENABLED: z.string().default("0"), + WEBHOOK_DELIVERIES_REPLICATION_SLOT_NAME: z + .string() + .default("webhook_deliveries_to_clickhouse_v1"), + WEBHOOK_DELIVERIES_REPLICATION_PUBLICATION_NAME: z + .string() + .default("webhook_deliveries_to_clickhouse_v1_publication"), + WEBHOOK_DELIVERIES_REPLICATION_MAX_FLUSH_CONCURRENCY: z.coerce.number().int().default(1), + WEBHOOK_DELIVERIES_REPLICATION_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000), + WEBHOOK_DELIVERIES_REPLICATION_FLUSH_BATCH_SIZE: z.coerce.number().int().default(100), + WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_TIMEOUT_MS: z.coerce.number().int().default(30_000), + WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_EXTEND_INTERVAL_MS: z.coerce + .number() + .int() + .default(10_000), + WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_ADDITIONAL_TIME_MS: z.coerce + .number() + .int() + .default(10_000), + WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_RETRY_INTERVAL_MS: z.coerce + .number() + .int() + .default(500), + WEBHOOK_DELIVERIES_REPLICATION_ACK_INTERVAL_SECONDS: z.coerce.number().int().default(10), + WEBHOOK_DELIVERIES_REPLICATION_LOG_LEVEL: z + .enum(["log", "error", "warn", "info", "debug"]) + .default("info"), + WEBHOOK_DELIVERIES_REPLICATION_WAIT_FOR_ASYNC_INSERT: z.string().default("0"), + WEBHOOK_DELIVERIES_REPLICATION_INSERT_STRATEGY: z + .enum(["insert", "insert_async"]) + .default("insert"), + WEBHOOK_DELIVERIES_REPLICATION_INSERT_MAX_RETRIES: z.coerce.number().int().default(3), + WEBHOOK_DELIVERIES_REPLICATION_INSERT_BASE_DELAY_MS: z.coerce.number().int().default(100), + WEBHOOK_DELIVERIES_REPLICATION_INSERT_MAX_DELAY_MS: z.coerce.number().int().default(2000), + // Clickhouse CLICKHOUSE_URL: z.string(), // Optional read replica endpoint. Read-only clients (logs, query, admin, runsList, diff --git a/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts new file mode 100644 index 00000000000..d1c247939d3 --- /dev/null +++ b/apps/webapp/app/presenters/v3/ApiWebhookDeliveryPresenter.server.ts @@ -0,0 +1,162 @@ +import { + type WebhookDeliveryListItem as ApiWebhookDeliveryListItem, + type WebhookDeliveryObject, +} from "@trigger.dev/core/v3"; +import { type WebhookDeliveryStatus } from "@trigger.dev/database"; +import { z } from "zod"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { type ApiAuthenticationResultSuccess } from "~/services/apiAuth.server"; +import { CoercedDate } from "~/utils/zod"; +import { BasePresenter } from "./basePresenter.server"; +import { WebhookDeliveriesListPresenter } from "./WebhookDeliveriesListPresenter.server"; +import { WebhookDeliveryDetailPresenter } from "./WebhookDeliveryDetailPresenter.server"; +import { type WebhookDeliveryListItem } from "./WebhookDetailPresenter.server"; + +const DB_STATUS_TO_API: Record = { + PENDING: "pending", + PROCESSING: "processing", + SUCCEEDED: "succeeded", + FAILED: "failed", + FILTERED: "filtered", +}; + +// API status -> DB status (for the filter). +const API_STATUS_TO_DB: Record = { + pending: "PENDING", + processing: "PROCESSING", + succeeded: "SUCCEEDED", + failed: "FAILED", + filtered: "FILTERED", +}; + +function toApiListItem(d: WebhookDeliveryListItem): ApiWebhookDeliveryListItem { + return { + id: d.friendlyId, + webhook: d.webhook?.slug ?? null, + status: DB_STATUS_TO_API[d.status], + externalDeliveryId: d.externalDeliveryId, + runId: d.run?.friendlyId ?? null, + createdAt: d.createdAt, + processedAt: d.processedAt, + }; +} + +export const ApiWebhookDeliveryListSearchParams = z.object({ + "page[size]": z.coerce.number().int().positive().min(1).max(100).optional(), + "page[after]": z.string().optional(), + "page[before]": z.string().optional(), + "filter[webhook]": z + .string() + .optional() + .transform((value) => (value ? value.split(",") : undefined)), + "filter[status]": z + .string() + .optional() + .transform((value, ctx) => { + if (!value) return undefined; + const statuses = value.split(","); + const invalid = statuses.filter( + (s) => !Object.prototype.hasOwnProperty.call(API_STATUS_TO_DB, s) + ); + if (invalid.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid status values: ${invalid.join( + ", " + )}. Allowed: pending, processing, succeeded, failed.`, + }); + return z.NEVER; + } + return Array.from(new Set(statuses.map((s) => API_STATUS_TO_DB[s]))); + }), + "filter[period]": z.string().optional(), + "filter[from]": CoercedDate, + "filter[to]": CoercedDate, +}); +export type ApiWebhookDeliveryListSearchParams = z.infer; + +export class ApiWebhookDeliveryListPresenter extends BasePresenter { + public async call( + environment: { id: string; projectId: string; organizationId: string }, + searchParams: ApiWebhookDeliveryListSearchParams + ): Promise<{ + data: ApiWebhookDeliveryListItem[]; + pagination: { next?: string; previous?: string }; + }> { + return this.trace("call", async () => { + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + environment.organizationId, + "standard" + ); + + const presenter = new WebhookDeliveriesListPresenter(this._replica, clickhouse); + const result = await presenter.call({ + organizationId: environment.organizationId, + projectId: environment.projectId, + environmentId: environment.id, + webhooks: searchParams["filter[webhook]"], + statuses: searchParams["filter[status]"], + period: searchParams["filter[period]"], + from: searchParams["filter[from]"]?.getTime(), + to: searchParams["filter[to]"]?.getTime(), + cursor: searchParams["page[after]"] ?? searchParams["page[before]"], + direction: searchParams["page[before]"] ? "backward" : "forward", + pageSize: searchParams["page[size]"], + }); + + return { data: result.deliveries.map(toApiListItem), pagination: result.pagination }; + }); + } +} + +export class ApiWebhookDeliveryPresenter extends BasePresenter { + public async call( + environment: { id: string; projectId: string; organizationId: string }, + deliveryFriendlyId: string + ): Promise { + return this.trace("call", async () => { + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + environment.organizationId, + "standard" + ); + + const presenter = new WebhookDeliveryDetailPresenter(this._replica, clickhouse); + const d = await presenter.call({ + organizationId: environment.organizationId, + projectId: environment.projectId, + environmentId: environment.id, + deliveryFriendlyId, + }); + + if (!d) return undefined; + + return { + id: d.friendlyId, + webhook: d.webhook?.slug ?? null, + status: DB_STATUS_TO_API[d.status], + externalDeliveryId: d.externalDeliveryId, + runId: d.run?.friendlyId ?? null, + createdAt: d.createdAt, + processedAt: d.processedAt, + idempotencyKey: d.idempotencyKey, + event: d.parsedEvent ?? null, + headers: (d.headers as Record | null) ?? null, + rawBodyHash: d.rawBodyHash, + error: d.errorMessage, + filterReason: d.filterReason, + updatedAt: d.updatedAt, + }; + }); + } +} + +export function findWebhookDeliveryResource( + authentication: ApiAuthenticationResultSuccess, + deliveryId: string +): Promise { + const env = authentication.environment; + return new ApiWebhookDeliveryPresenter().call( + { id: env.id, projectId: env.projectId, organizationId: env.organizationId }, + deliveryId + ); +} diff --git a/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts new file mode 100644 index 00000000000..80a573a0f9b --- /dev/null +++ b/apps/webapp/app/presenters/v3/ApiWebhookEndpointPresenter.server.ts @@ -0,0 +1,105 @@ +import { type WebhookEndpointObject } from "@trigger.dev/core/v3"; +import { + type Prisma, + type RuntimeEnvironment, + type WebhookEndpointStatus, +} from "@trigger.dev/database"; +import { z } from "zod"; +import { webhookReplica } from "~/db.server"; +import { type ApiAuthenticationResultSuccess } from "~/services/apiAuth.server"; +import { webhookIngressUrl } from "~/utils/webhookIngressUrl.server"; +import { BasePresenter } from "./basePresenter.server"; + +const DB_STATUS_TO_API: Record = { + ACTIVE: "active", + INACTIVE: "inactive", + DELETING: "deleting", +}; + +// The columns needed to build the public API object. +const endpointSelect = { + friendlyId: true, + opaqueId: true, + handlerWebhookId: true, + source: true, + status: true, + secretProvisioning: true, + signingSecretKey: true, + endpointTenantId: true, + endpointExternalRef: true, + createdAt: true, + updatedAt: true, +} satisfies Prisma.WebhookEndpointSelect; + +type EndpointRow = Prisma.WebhookEndpointGetPayload<{ select: typeof endpointSelect }>; + +function toApiEndpoint(endpoint: EndpointRow): WebhookEndpointObject { + return { + id: endpoint.friendlyId, + webhook: endpoint.handlerWebhookId, + source: endpoint.source, + status: DB_STATUS_TO_API[endpoint.status], + secretProvisioning: + (endpoint.secretProvisioning as WebhookEndpointObject["secretProvisioning"]) ?? "either", + secretSet: endpoint.signingSecretKey != null && endpoint.signingSecretKey !== "", + tenantId: endpoint.endpointTenantId === "" ? null : endpoint.endpointTenantId, + externalRef: endpoint.endpointExternalRef === "" ? null : endpoint.endpointExternalRef, + url: webhookIngressUrl(endpoint.opaqueId), + createdAt: endpoint.createdAt, + updatedAt: endpoint.updatedAt, + }; +} + +export const ApiWebhookEndpointListSearchParams = z.object({ + "filter[webhook]": z + .string() + .optional() + .transform((value) => (value ? value.split(",") : undefined)), +}); +export type ApiWebhookEndpointListSearchParams = z.infer; + +export class ApiWebhookEndpointListPresenter extends BasePresenter { + public async call( + environment: Pick, + searchParams: ApiWebhookEndpointListSearchParams + ): Promise<{ data: WebhookEndpointObject[] }> { + return this.trace("call", async () => { + const endpoints = await webhookReplica.webhookEndpoint.findMany({ + where: { + runtimeEnvironmentId: environment.id, + ...(searchParams["filter[webhook]"] + ? { handlerWebhookId: { in: searchParams["filter[webhook]"] } } + : {}), + }, + select: endpointSelect, + orderBy: [{ handlerWebhookId: "asc" }, { createdAt: "desc" }], + }); + + return { data: endpoints.map(toApiEndpoint) }; + }); + } +} + +export class ApiWebhookEndpointPresenter extends BasePresenter { + public async call( + environmentId: string, + endpointFriendlyId: string + ): Promise { + return this.trace("call", async () => { + const endpoint = await webhookReplica.webhookEndpoint.findFirst({ + // friendlyId is globally unique; scope to the env so a foreign id 404s. + where: { friendlyId: endpointFriendlyId, runtimeEnvironmentId: environmentId }, + select: endpointSelect, + }); + + return endpoint ? toApiEndpoint(endpoint) : undefined; + }); + } +} + +export function findWebhookEndpointResource( + authentication: ApiAuthenticationResultSuccess, + endpointId: string +): Promise { + return new ApiWebhookEndpointPresenter().call(authentication.environment.id, endpointId); +} diff --git a/apps/webapp/app/presenters/v3/TestPresenter.server.ts b/apps/webapp/app/presenters/v3/TestPresenter.server.ts index 8555b8117e5..eb684114a4c 100644 --- a/apps/webapp/app/presenters/v3/TestPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestPresenter.server.ts @@ -52,7 +52,7 @@ export class TestPresenter extends BasePresenter { SELECT bwt.id, version, slug, "filePath", bwt."friendlyId", bwt."triggerSource" FROM latest_workers JOIN ${sqlDatabaseSchema}."BackgroundWorkerTask" bwt ON bwt."workerId" = latest_workers.id - WHERE bwt."triggerSource" != 'AGENT' + WHERE bwt."triggerSource" NOT IN ('AGENT') ORDER BY slug ASC;`; } else { const currentDeployment = await findCurrentWorkerDeployment({ environmentId: envId }); diff --git a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts index 430477ce582..1618e8cab7e 100644 --- a/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/TestTaskPresenter.server.ts @@ -107,6 +107,10 @@ export type TestTaskResult = allowArbitraryQueues: boolean; taskRunTemplates: TaskRunTemplate[]; } + | { + foundTask: true; + triggerSource: "WEBHOOK"; + } | { foundTask: false; }; @@ -119,7 +123,6 @@ export type ScheduledTaskResult = Extract< TestTaskResult, { foundTask: true; triggerSource: "SCHEDULED" } >; - type RawRun = { id: string; queue: string; @@ -385,6 +388,9 @@ export class TestTaskPresenter { // AGENT tasks are filtered out by TestPresenter and shouldn't reach here return { foundTask: false }; } + case "WEBHOOK": { + return { foundTask: true, triggerSource: "WEBHOOK" }; + } default: { return task.triggerSource satisfies never; } diff --git a/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts b/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts index 6f98e154477..f0508d9eed0 100644 --- a/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/UnifiedTaskListPresenter.server.ts @@ -13,7 +13,7 @@ import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.s import { agentListPresenter, type AgentActiveState } from "./AgentListPresenter.server"; import { taskListPresenter, type TaskListItem } from "./TaskListPresenter.server"; -export type UnifiedTaskKind = "STANDARD" | "SCHEDULED" | "AGENT"; +export type UnifiedTaskKind = "STANDARD" | "SCHEDULED" | "AGENT" | "WEBHOOK"; export type UnifiedTaskListItem = { kind: UnifiedTaskKind; @@ -215,7 +215,12 @@ function toUnifiedItems( for (const task of tasks) { items.push({ - kind: task.triggerSource === "SCHEDULED" ? "SCHEDULED" : "STANDARD", + kind: + task.triggerSource === "SCHEDULED" + ? "SCHEDULED" + : task.triggerSource === "WEBHOOK" + ? "WEBHOOK" + : "STANDARD", slug: task.slug, filePath: task.filePath, triggerSource: task.triggerSource, diff --git a/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts new file mode 100644 index 00000000000..046e9850429 --- /dev/null +++ b/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts @@ -0,0 +1,155 @@ +import { type ClickHouse } from "@internal/clickhouse"; +import { type PrismaClientOrTransaction, type WebhookDeliveryStatus } from "@trigger.dev/database"; +import parseDuration from "parse-duration"; +import { webhookReplica } from "~/db.server"; +import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; +import { + resolveDeliveryRunTargets, + type WebhookDeliveryListItem, +} from "./WebhookDetailPresenter.server"; + +const DELIVERIES_PAGE_SIZE = 25; +type Direction = "forward" | "backward"; + +export type WebhookDeliveriesListResult = { + deliveries: WebhookDeliveryListItem[]; + pagination: { next?: string; previous?: string }; +}; + +/** + * The top-level (cross-endpoint) deliveries list: every delivery in the environment, + * across all webhook endpoints/handlers. Mirrors WebhookDetailPresenter.listDeliveries but + * with no webhookEndpointId filter, plus it hydrates which webhook each delivery belongs to + * for the table's Webhook column. + */ +export class WebhookDeliveriesListPresenter { + constructor( + private readonly replica: PrismaClientOrTransaction, + private readonly clickhouse: ClickHouse + ) {} + + async call({ + organizationId, + projectId, + environmentId, + webhooks, + statuses, + deliveryId, + runId, + isTest, + period, + from, + to, + cursor, + direction, + pageSize, + }: { + organizationId: string; + projectId: string; + environmentId: string; + webhooks?: string[]; + statuses?: WebhookDeliveryStatus[]; + deliveryId?: string; + runId?: string; + isTest?: boolean; + period?: string; + from?: number; + to?: number; + cursor?: string; + direction?: Direction; + pageSize?: number; + }): Promise { + const periodMs = period ? (parseDuration(period) ?? undefined) : undefined; + + // Resolve the handler-slug webhook filter to endpoint ids. A non-empty webhook + // filter that matches no endpoints must return nothing, so fall back to a + // sentinel id that can never match rather than dropping the filter entirely. + let webhookEndpointIds: string[] | undefined; + if (webhooks && webhooks.length > 0) { + const endpoints = await webhookReplica.webhookEndpoint.findMany({ + where: { runtimeEnvironmentId: environmentId, handlerWebhookId: { in: webhooks } }, + select: { id: true }, + }); + webhookEndpointIds = endpoints.length > 0 ? endpoints.map((e) => e.id) : ["__none__"]; + } + + // The runId param is a FRIENDLY run id; the deliveries store the INTERNAL id. + // Resolve it, and force empty results when no run matches. + let internalRunId: string | undefined; + if (runId) { + const run = await this.replica.taskRun.findFirst({ + where: { friendlyId: runId }, + select: { id: true }, + }); + internalRunId = run?.id ?? "__none__"; + } + + // Built per request (factory, NOT a singleton), matching every RunsRepository consumer. + const repository = webhookDeliveriesRepository({ + clickhouse: this.clickhouse, + prisma: webhookReplica, + }); + + // No webhookEndpointId: all endpoints in the environment. + const { deliveries, pagination } = await repository.listDeliveries({ + organizationId, + projectId, + environmentId, + webhookEndpointIds, + deliveryId, + runId: internalRunId, + statuses, + isTest, + period: periodMs, + from, + to, + page: { size: pageSize ?? DELIVERIES_PAGE_SIZE, cursor, direction }, + }); + + // Resolve run friendlyIds (the runId is the INTERNAL id; the table links by friendlyId) and, for + // session deliveries, the session the run belongs to. + const { runFriendlyIdById, sessionByRunId } = await resolveDeliveryRunTargets( + this.replica, + deliveries + ); + + // Resolve which webhook (handler) each delivery belongs to for the Webhook column. + const endpointIds = Array.from(new Set(deliveries.map((d) => d.webhookEndpointId))); + const endpointById = new Map(); + if (endpointIds.length > 0) { + const endpoints = await webhookReplica.webhookEndpoint.findMany({ + where: { id: { in: endpointIds } }, + select: { id: true, handlerWebhookId: true, source: true }, + }); + for (const e of endpoints) { + endpointById.set(e.id, { slug: e.handlerWebhookId, source: e.source }); + } + } + + const items: WebhookDeliveryListItem[] = deliveries.map((d) => { + const friendlyId = d.runId ? runFriendlyIdById.get(d.runId) : undefined; + return { + id: d.id, + friendlyId: d.friendlyId, + externalDeliveryId: d.externalDeliveryId, + status: d.status, + isTest: d.isTest, + runId: d.runId, + run: friendlyId ? { friendlyId } : null, + session: d.runId ? (sessionByRunId.get(d.runId) ?? null) : null, + errorMessage: d.errorMessage, + createdAt: d.createdAt, + processedAt: d.processedAt, + webhook: endpointById.get(d.webhookEndpointId) ?? null, + }; + }); + + return { + deliveries: items, + pagination: { + next: pagination.nextCursor ?? undefined, + previous: pagination.previousCursor ?? undefined, + }, + }; + } +} diff --git a/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts new file mode 100644 index 00000000000..0f4d3ceed04 --- /dev/null +++ b/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts @@ -0,0 +1,122 @@ +import { type ClickHouse } from "@internal/clickhouse"; +import { + type Prisma, + type PrismaClientOrTransaction, + type RuntimeEnvironmentType, + type WebhookDeliveryStatus, +} from "@trigger.dev/database"; +import { webhookReplica } from "~/db.server"; +import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; + +export type WebhookDeliveryDetail = { + id: string; + friendlyId: string; + status: WebhookDeliveryStatus; + externalDeliveryId: string; + idempotencyKey: string; + rawBodyHash: string | null; + parsedEvent: Prisma.JsonValue | null; + headers: Prisma.JsonValue | null; + errorMessage: string | null; + filterReason: string | null; + environmentType: RuntimeEnvironmentType; + createdAt: Date; + updatedAt: Date; + processedAt: Date | null; + // Resolved from the run id (deliveries store the INTERNAL id; the UI links by friendlyId). + run: { friendlyId: string } | null; + // Set when the run belongs to a chat.agent session (the delivery routed to a session); the session + // is the meaningful target, so the UI links it instead of the incidental run. + session: { friendlyId: string; externalId: string | null } | null; + // The handler webhook this delivery routed to. + webhook: { slug: string; source: string } | null; +}; + +/** + * A single webhook delivery, by its friendlyId. ClickHouse resolves the friendlyId + * to (id, createdAt) for a partition-pruned Postgres point lookup, then this hydrates + * the run friendlyId and the handler webhook for the detail view. + */ +export class WebhookDeliveryDetailPresenter { + constructor( + private readonly replica: PrismaClientOrTransaction, + private readonly clickhouse: ClickHouse + ) {} + + async call({ + organizationId, + projectId, + environmentId, + deliveryFriendlyId, + }: { + organizationId: string; + projectId: string; + environmentId: string; + deliveryFriendlyId: string; + }): Promise { + const repository = webhookDeliveriesRepository({ + clickhouse: this.clickhouse, + prisma: webhookReplica, + }); + + const delivery = await repository.getDelivery({ + organizationId, + projectId, + environmentId, + friendlyId: deliveryFriendlyId, + }); + + if (!delivery) { + return null; + } + + // Defense in depth: getDelivery is already env-scoped via ClickHouse, but the + // Postgres row is the source of truth, so confirm it belongs to this environment. + if (delivery.runtimeEnvironmentId !== environmentId) { + return null; + } + + let run: { friendlyId: string } | null = null; + let session: { friendlyId: string; externalId: string | null } | null = null; + if (delivery.runId) { + const [taskRun, sessionRun] = await Promise.all([ + this.replica.taskRun.findFirst({ + where: { id: delivery.runId }, + select: { friendlyId: true }, + }), + this.replica.sessionRun.findUnique({ + where: { runId: delivery.runId }, + select: { session: { select: { friendlyId: true, externalId: true } } }, + }), + ]); + run = taskRun ? { friendlyId: taskRun.friendlyId } : null; + session = sessionRun?.session ?? null; + } + + const endpoint = await webhookReplica.webhookEndpoint.findFirst({ + where: { id: delivery.webhookEndpointId }, + select: { handlerWebhookId: true, source: true }, + }); + const webhook = endpoint ? { slug: endpoint.handlerWebhookId, source: endpoint.source } : null; + + return { + id: delivery.id, + friendlyId: delivery.friendlyId, + status: delivery.status, + externalDeliveryId: delivery.externalDeliveryId, + idempotencyKey: delivery.idempotencyKey, + rawBodyHash: delivery.rawBodyHash, + parsedEvent: delivery.parsedEvent, + headers: delivery.headers, + errorMessage: delivery.errorMessage, + filterReason: delivery.filterReason, + environmentType: delivery.environmentType, + createdAt: delivery.createdAt, + updatedAt: delivery.updatedAt, + processedAt: delivery.processedAt, + run, + session, + webhook, + }; + } +} diff --git a/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts new file mode 100644 index 00000000000..c03547a6271 --- /dev/null +++ b/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts @@ -0,0 +1,645 @@ +import { type ClickHouse } from "@internal/clickhouse"; +import { + type Prisma, + type PrismaClientOrTransaction, + type RuntimeEnvironmentType, + type WebhookDeliveryStatus, + type WebhookEndpointStatus, +} from "@trigger.dev/database"; +import parseDuration from "parse-duration"; +import { z } from "zod"; +import { type Direction } from "~/components/ListPagination"; +import { webhookReplica } from "~/db.server"; +import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; +import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; +import { + buildWebhookComposerEndpoints, + type WebhookComposerEndpointData, +} from "./webhookComposerEndpoints.server"; + +export type WebhookEndpointSummary = { + id: string; + opaqueId: string; + status: string; + hasSigningSecret: boolean; +}; + +export type WebhookDetail = { + slug: string; + filePath: string; + triggerSource: "WEBHOOK"; + source: string; + metadata: unknown; + createdAt: Date; + endpoint: WebhookEndpointSummary; +}; + +export type WebhookActivityPoint = { + bucket: number; // epoch ms +} & Record; + +export type WebhookActivity = { + data: WebhookActivityPoint[]; + statuses: string[]; +}; + +export type WebhookDeliveryListItem = { + id: string; + friendlyId: string; + externalDeliveryId: string; + status: WebhookDeliveryStatus; + isTest: boolean; + runId: string | null; + run: { friendlyId: string } | null; + // Set when the delivery routed to a chat.agent session (the run belongs to a session). The session + // is the meaningful target here, so the table links it instead of the incidental run. + session: { friendlyId: string; externalId: string | null } | null; + errorMessage: string | null; + createdAt: Date; + processedAt: Date | null; + // Only populated by the cross-endpoint (top-level) deliveries list, where the + // table shows which webhook each delivery belongs to. Undefined on the scoped + // per-webhook detail page. + webhook?: { slug: string; source: string } | null; +}; + +export type WebhookDeliveriesList = { + deliveries: WebhookDeliveryListItem[]; + pagination: { next?: string; previous?: string }; + filters: { from?: number; to?: number }; + hasFilters: boolean; +}; + +/** + * Resolve delivery run ids (INTERNAL, no FK) to their run friendlyId and, when the run belongs to a + * chat.agent session, the session it targeted. Shared by the per-endpoint and cross-endpoint lists. + */ +export async function resolveDeliveryRunTargets( + replica: PrismaClientOrTransaction, + deliveries: { runId: string | null }[] +): Promise<{ + runFriendlyIdById: Map; + sessionByRunId: Map; +}> { + const runIds = Array.from( + new Set(deliveries.map((d) => d.runId).filter((id): id is string => Boolean(id))) + ); + const runFriendlyIdById = new Map(); + const sessionByRunId = new Map(); + if (runIds.length === 0) return { runFriendlyIdById, sessionByRunId }; + + const [runs, sessionRuns] = await Promise.all([ + replica.taskRun.findMany({ + where: { id: { in: runIds } }, + select: { id: true, friendlyId: true }, + }), + replica.sessionRun.findMany({ + where: { runId: { in: runIds } }, + select: { runId: true, session: { select: { friendlyId: true, externalId: true } } }, + }), + ]); + for (const run of runs) runFriendlyIdById.set(run.id, run.friendlyId); + for (const sr of sessionRuns) sessionByRunId.set(sr.runId, sr.session); + return { runFriendlyIdById, sessionByRunId }; +} + +export type WebhookEndpointListItem = { + friendlyId: string; + // The declared default endpoint (no tenant/externalRef scope). + isDefault: boolean; + tenantId: string | null; + externalRef: string | null; + status: WebhookEndpointStatus; + hasSigningSecret: boolean; + deliveryCount: number; +}; + +export type WebhookEndpointDetail = { + id: string; + friendlyId: string; + opaqueId: string; + handlerWebhookId: string; + source: string; + status: WebhookEndpointStatus; + isDefault: boolean; + tenantId: string; + externalRef: string; + hasSigningSecret: boolean; + // "provider" | "integrator" | "either" — drives the Connect UI (paste vs generate). + secretProvisioning: string; + // Tagged-union JSON parsed by the route with the @trigger.dev/core schemas. + routingTarget: Prisma.JsonValue; + verifierArtifact: Prisma.JsonValue; + metadata: Prisma.JsonValue; + createdAt: Date; + updatedAt: Date; +}; + +// 7-day rolling window for the per-endpoint delivery counts on the Endpoints tab. +const ENDPOINT_DELIVERY_COUNT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; + +// Run-status group order, shared with getRunActivity. Mirrors AgentDetailPresenter. +const TERMINAL_GROUPS = { + COMPLETED: ["COMPLETED_SUCCESSFULLY"], + FAILED: ["COMPLETED_WITH_ERRORS", "SYSTEM_FAILURE", "CRASHED", "INTERRUPTED", "TIMED_OUT"], + CANCELED: ["CANCELED", "EXPIRED"], + RUNNING: [ + "EXECUTING", + "DEQUEUED", + "PENDING_EXECUTING", + "WAITING_TO_RESUME", + "QUEUED_EXECUTING", + "PENDING", + "PENDING_VERSION", + "DELAYED", + "WAITING_FOR_DEPLOY", + ], +} as const; + +const GROUP_LABEL = ["COMPLETED", "FAILED", "CANCELED", "RUNNING"] as const; +type GroupLabel = (typeof GROUP_LABEL)[number]; + +function groupForStatus(status: string): GroupLabel | undefined { + for (const label of GROUP_LABEL) { + if ((TERMINAL_GROUPS[label] as readonly string[]).includes(status)) return label; + } + return undefined; +} + +// Stable legend order for the deliveries activity chart. +const DELIVERY_STATUSES = ["PENDING", "PROCESSING", "SUCCEEDED", "FAILED"] as const; + +const DELIVERIES_PAGE_SIZE = 25; + +export class WebhookDetailPresenter { + constructor( + private readonly replica: PrismaClientOrTransaction, + private readonly clickhouse: ClickHouse + ) {} + + async findWebhook({ + environmentId, + environmentType, + webhookSlug, + }: { + environmentId: string; + environmentType: RuntimeEnvironmentType; + webhookSlug: string; + }): Promise { + const currentWorker = await findCurrentWorkerFromEnvironment( + { id: environmentId, type: environmentType }, + this.replica + ); + + if (!currentWorker) return null; + + const task = await this.replica.backgroundWorkerTask.findFirst({ + where: { + workerId: currentWorker.id, + slug: webhookSlug, + triggerSource: "WEBHOOK", + }, + select: { + slug: true, + filePath: true, + triggerSource: true, + createdAt: true, + }, + }); + + if (!task) return null; + + const endpoint = await webhookReplica.webhookEndpoint.findFirst({ + where: { + runtimeEnvironmentId: environmentId, + handlerWebhookId: webhookSlug, + endpointTenantId: "", + endpointExternalRef: "", + }, + select: { + id: true, + opaqueId: true, + status: true, + source: true, + metadata: true, + // signingSecretKey is selected ONLY to derive hasSigningSecret below. + // The secret value never leaves this method. + signingSecretKey: true, + }, + }); + + if (!endpoint) return null; + + return { + slug: task.slug, + filePath: task.filePath, + triggerSource: "WEBHOOK", + source: endpoint.source, + metadata: endpoint.metadata, + createdAt: task.createdAt, + endpoint: { + id: endpoint.id, + opaqueId: endpoint.opaqueId, + status: endpoint.status, + hasSigningSecret: endpoint.signingSecretKey != null && endpoint.signingSecretKey !== "", + }, + }; + } + + async listEndpoints({ + organizationId, + projectId, + environmentId, + handlerWebhookId, + }: { + organizationId: string; + projectId: string; + environmentId: string; + handlerWebhookId: string; + }): Promise { + const endpoints = await webhookReplica.webhookEndpoint.findMany({ + where: { runtimeEnvironmentId: environmentId, handlerWebhookId }, + select: { + id: true, + friendlyId: true, + endpointTenantId: true, + endpointExternalRef: true, + status: true, + signingSecretKey: true, + createdAt: true, + }, + // Default endpoint (empty scope) first, then most recent. + orderBy: [{ endpointTenantId: "asc" }, { createdAt: "desc" }], + }); + + const repository = webhookDeliveriesRepository({ + clickhouse: this.clickhouse, + prisma: webhookReplica, + }); + + // Per-endpoint 7d delivery counts in ONE grouped CH query (not an N+1 of count queries). + // Degrade to empty (0 per endpoint) on error rather than failing the whole tab. + const deliveryCounts = await repository + .countDeliveriesByEndpoint({ + organizationId, + projectId, + environmentId, + webhookEndpointIds: endpoints.map((endpoint) => endpoint.id), + period: ENDPOINT_DELIVERY_COUNT_WINDOW_MS, + }) + .catch(() => new Map()); + + return endpoints.map((endpoint) => { + const isDefault = endpoint.endpointTenantId === "" && endpoint.endpointExternalRef === ""; + + return { + friendlyId: endpoint.friendlyId, + isDefault, + tenantId: endpoint.endpointTenantId === "" ? null : endpoint.endpointTenantId, + externalRef: endpoint.endpointExternalRef === "" ? null : endpoint.endpointExternalRef, + status: endpoint.status, + hasSigningSecret: endpoint.signingSecretKey != null && endpoint.signingSecretKey !== "", + deliveryCount: deliveryCounts.get(endpoint.id) ?? 0, + } satisfies WebhookEndpointListItem; + }); + } + + async listComposerEndpoints({ + environmentId, + handlerWebhookId, + }: { + environmentId: string; + handlerWebhookId: string; + }): Promise { + const endpoints = await webhookReplica.webhookEndpoint.findMany({ + where: { runtimeEnvironmentId: environmentId, handlerWebhookId }, + select: { + friendlyId: true, + opaqueId: true, + source: true, + endpointTenantId: true, + endpointExternalRef: true, + verifierArtifact: true, + signingSecretKey: true, + }, + orderBy: [{ endpointTenantId: "asc" }, { createdAt: "desc" }], + }); + return buildWebhookComposerEndpoints(endpoints); + } + + async findEndpoint({ + environmentId, + endpointFriendlyId, + }: { + environmentId: string; + endpointFriendlyId: string; + }): Promise { + const endpoint = await webhookReplica.webhookEndpoint.findFirst({ + // friendlyId is globally unique, but scope to the env so a foreign id 404s. + where: { friendlyId: endpointFriendlyId, runtimeEnvironmentId: environmentId }, + select: { + id: true, + friendlyId: true, + opaqueId: true, + handlerWebhookId: true, + source: true, + status: true, + endpointTenantId: true, + endpointExternalRef: true, + signingSecretKey: true, + secretProvisioning: true, + routingTarget: true, + verifierArtifact: true, + metadata: true, + createdAt: true, + updatedAt: true, + }, + }); + + if (!endpoint) return null; + + return { + id: endpoint.id, + friendlyId: endpoint.friendlyId, + opaqueId: endpoint.opaqueId, + handlerWebhookId: endpoint.handlerWebhookId, + source: endpoint.source, + status: endpoint.status, + isDefault: endpoint.endpointTenantId === "" && endpoint.endpointExternalRef === "", + tenantId: endpoint.endpointTenantId, + externalRef: endpoint.endpointExternalRef, + hasSigningSecret: endpoint.signingSecretKey != null && endpoint.signingSecretKey !== "", + secretProvisioning: endpoint.secretProvisioning, + routingTarget: endpoint.routingTarget, + verifierArtifact: endpoint.verifierArtifact, + metadata: endpoint.metadata, + createdAt: endpoint.createdAt, + updatedAt: endpoint.updatedAt, + }; + } + + async getRunActivity({ + organizationId, + projectId, + environmentId, + webhookSlug, + from, + to, + }: { + organizationId: string; + projectId: string; + environmentId: string; + webhookSlug: string; + from: Date; + to: Date; + }): Promise { + const rangeMs = Math.max(1, to.getTime() - from.getTime()); + const oneHour = 60 * 60 * 1000; + const oneDay = 24 * oneHour; + + const bucketSeconds = + rangeMs <= oneDay ? 60 * 60 : rangeMs <= 7 * oneDay ? 6 * 60 * 60 : 24 * 60 * 60; + + // FINAL + _is_deleted = 0 because task_runs_v2 is a ReplacingMergeTree; + // org/project filters engage the sort-key prefix for partition pruning. + const queryFn = this.clickhouse.reader.query({ + name: "webhookRunStatusActivity", + query: `SELECT + toUnixTimestamp(toStartOfInterval(created_at, INTERVAL {bucketSeconds: UInt32} SECOND)) AS bucket, + status, + count() AS val + FROM trigger_dev.task_runs_v2 FINAL + WHERE organization_id = {organizationId: String} + AND project_id = {projectId: String} + AND environment_id = {environmentId: String} + AND task_identifier = {webhookSlug: String} + AND created_at >= {fromTime: DateTime64(3, 'UTC')} + AND created_at < {toTime: DateTime64(3, 'UTC')} + AND _is_deleted = 0 + GROUP BY bucket, status + ORDER BY bucket`, + params: z.object({ + organizationId: z.string(), + projectId: z.string(), + environmentId: z.string(), + webhookSlug: z.string(), + bucketSeconds: z.number(), + fromTime: z.string(), + toTime: z.string(), + }), + schema: z.object({ + bucket: z.coerce.number(), + status: z.string(), + val: z.coerce.number(), + }), + }); + + const [error, rows] = await queryFn({ + organizationId, + projectId, + environmentId, + webhookSlug, + bucketSeconds, + // ClickHouse's DateTime64(3, 'UTC') parser rejects the trailing `Z` from + // JS toISOString(). Strip it. + fromTime: from.toISOString().slice(0, -1), + toTime: to.toISOString().slice(0, -1), + }); + + if (error) { + console.error("Webhook run activity query failed:", error); + return { data: [], statuses: [] }; + } + + const bucketMap = new Map>(); + for (const row of rows) { + const group = groupForStatus(row.status) ?? "RUNNING"; + const ts = row.bucket * 1000; + const existing = bucketMap.get(ts) ?? {}; + existing[group] = (existing[group] ?? 0) + row.val; + bucketMap.set(ts, existing); + } + + const bucketMs = bucketSeconds * 1000; + const start = Math.floor(from.getTime() / bucketMs) * bucketMs; + const end = Math.ceil(to.getTime() / bucketMs) * bucketMs; + const points: WebhookActivityPoint[] = []; + const orderedStatuses = [...GROUP_LABEL]; + for (let ts = start; ts < end; ts += bucketMs) { + const existing = bucketMap.get(ts) ?? {}; + const point: WebhookActivityPoint = { bucket: ts }; + for (const g of orderedStatuses) { + point[g] = existing[g] ?? 0; + } + points.push(point); + } + + return { data: points, statuses: orderedStatuses }; + } + + async getDeliveryActivity({ + organizationId, + projectId, + environmentId, + webhookEndpointId, + from, + to, + }: { + organizationId: string; + projectId: string; + environmentId: string; + webhookEndpointId: string; + from: Date; + to: Date; + }): Promise { + const rangeMs = Math.max(1, to.getTime() - from.getTime()); + const oneHour = 60 * 60 * 1000; + const oneDay = 24 * oneHour; + const bucketSeconds = rangeMs <= oneDay ? 3600 : rangeMs <= 7 * oneDay ? 6 * 3600 : 24 * 3600; + + const queryFn = this.clickhouse.reader.query({ + name: "webhookDeliveryStatusActivity", + query: `SELECT + toUnixTimestamp(toStartOfInterval(created_at, INTERVAL {bucketSeconds: UInt32} SECOND)) AS bucket, + status, count() AS val + FROM trigger_dev.webhook_deliveries_v1 FINAL + WHERE organization_id = {organizationId: String} + AND project_id = {projectId: String} + AND environment_id = {environmentId: String} + AND webhook_endpoint_id = {webhookEndpointId: String} + AND created_at >= {fromTime: DateTime64(3, 'UTC')} + AND created_at < {toTime: DateTime64(3, 'UTC')} + AND _is_deleted = 0 + GROUP BY bucket, status + ORDER BY bucket`, + params: z.object({ + organizationId: z.string(), + projectId: z.string(), + environmentId: z.string(), + webhookEndpointId: z.string(), + bucketSeconds: z.number(), + fromTime: z.string(), + toTime: z.string(), + }), + schema: z.object({ + bucket: z.coerce.number(), + status: z.string(), + val: z.coerce.number(), + }), + }); + + const [error, rows] = await queryFn({ + organizationId, + projectId, + environmentId, + webhookEndpointId, + bucketSeconds, + fromTime: from.toISOString().slice(0, -1), + toTime: to.toISOString().slice(0, -1), + }); + + if (error) { + console.error("Webhook delivery activity query failed:", error); + return { data: [], statuses: [] }; + } + + const bucketMap = new Map>(); + for (const row of rows) { + const ts = row.bucket * 1000; + const existing = bucketMap.get(ts) ?? {}; + existing[row.status] = (existing[row.status] ?? 0) + row.val; + bucketMap.set(ts, existing); + } + + const bucketMs = bucketSeconds * 1000; + const start = Math.floor(from.getTime() / bucketMs) * bucketMs; + const end = Math.ceil(to.getTime() / bucketMs) * bucketMs; + const points: WebhookActivityPoint[] = []; + const orderedStatuses = [...DELIVERY_STATUSES]; + for (let ts = start; ts < end; ts += bucketMs) { + const existing = bucketMap.get(ts) ?? {}; + const point: WebhookActivityPoint = { bucket: ts }; + for (const s of orderedStatuses) { + point[s] = existing[s] ?? 0; + } + points.push(point); + } + + return { data: points, statuses: orderedStatuses }; + } + + async listDeliveries({ + organizationId, + projectId, + environmentId, + webhookEndpointId, + period, + from, + to, + cursor, + direction, + }: { + organizationId: string; + projectId: string; + environmentId: string; + webhookEndpointId: string; + period?: string; + from?: number; + to?: number; + cursor?: string; + direction?: Direction; + }): Promise { + const periodMs = period ? (parseDuration(period) ?? undefined) : undefined; + + // Built per request (factory, NOT a singleton), matching every RunsRepository consumer. + const repository = webhookDeliveriesRepository({ + clickhouse: this.clickhouse, + prisma: webhookReplica, + }); + + const { deliveries, pagination } = await repository.listDeliveries({ + organizationId, + projectId, + environmentId, + webhookEndpointId, + period: periodMs, + from, + to, + page: { size: DELIVERIES_PAGE_SIZE, cursor, direction }, + }); + + // A delivery's runId is the INTERNAL run id (no FK); resolve friendlyIds and, for session + // deliveries, the session the run belongs to, with a small keyed lookup. + const { runFriendlyIdById, sessionByRunId } = await resolveDeliveryRunTargets( + this.replica, + deliveries + ); + + const items: WebhookDeliveryListItem[] = deliveries.map((d) => { + const friendlyId = d.runId ? runFriendlyIdById.get(d.runId) : undefined; + return { + id: d.id, + friendlyId: d.friendlyId, + externalDeliveryId: d.externalDeliveryId, + status: d.status, + isTest: d.isTest, + runId: d.runId, + run: friendlyId ? { friendlyId } : null, + session: d.runId ? (sessionByRunId.get(d.runId) ?? null) : null, + errorMessage: d.errorMessage, + createdAt: d.createdAt, + processedAt: d.processedAt, + }; + }); + + return { + deliveries: items, + pagination: { + next: pagination.nextCursor ?? undefined, + previous: pagination.previousCursor ?? undefined, + }, + filters: { from, to }, + hasFilters: Boolean(period || from || to), + }; + } +} diff --git a/apps/webapp/app/presenters/v3/webhookComposerEndpoints.server.ts b/apps/webapp/app/presenters/v3/webhookComposerEndpoints.server.ts new file mode 100644 index 00000000000..b406849eff5 --- /dev/null +++ b/apps/webapp/app/presenters/v3/webhookComposerEndpoints.server.ts @@ -0,0 +1,53 @@ +import { type WebhookHandshakeConfig, WebhookVerifierArtifact } from "@trigger.dev/core/v3"; +import { webhookIngressUrl } from "~/utils/webhookIngressUrl.server"; + +export type WebhookComposerEndpointData = { + friendlyId: string; + label: string; + source: string; + ingressUrl: string; + scheme: "hmac" | "shared-secret" | "url-secret" | "asymmetric"; + hasSigningSecret: boolean; + /** Present only when the endpoint declares a provider handshake (unlocks the handshake test). */ + handshake: WebhookHandshakeConfig | null; +}; + +type EndpointRow = { + friendlyId: string; + opaqueId: string; + source: string; + endpointTenantId: string; + endpointExternalRef: string; + verifierArtifact: unknown; + signingSecretKey: string | null; +}; + +/** + * Map raw WebhookEndpoint rows to the shape the composer consumes (label + scheme + ingress URL + + * handshake). Shared by the /test WEBHOOK arm and the console tab so the two stay in lockstep. + */ +export function buildWebhookComposerEndpoints(rows: EndpointRow[]): WebhookComposerEndpointData[] { + return rows.map((endpoint) => { + const parsed = WebhookVerifierArtifact.safeParse(endpoint.verifierArtifact); + const scheme = + parsed.success && parsed.data.kind !== "bundle" ? parsed.data.config.scheme : "hmac"; + const handshake = + parsed.success && parsed.data.kind !== "bundle" ? (parsed.data.handshake ?? null) : null; + + const isDefault = endpoint.endpointTenantId === "" && endpoint.endpointExternalRef === ""; + + return { + friendlyId: endpoint.friendlyId, + label: isDefault + ? "default" + : endpoint.endpointExternalRef + ? `${endpoint.endpointTenantId}: ${endpoint.endpointExternalRef}` + : endpoint.endpointTenantId, + source: endpoint.source, + ingressUrl: webhookIngressUrl(endpoint.opaqueId), + scheme, + hasSigningSecret: endpoint.signingSecretKey != null && endpoint.signingSecretKey !== "", + handshake, + } satisfies WebhookComposerEndpointData; + }); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index 99c73f33849..6d0eb58d694 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -16,6 +16,7 @@ import { PlusIcon } from "~/assets/icons/PlusIcon"; import { QuestionMarkIcon } from "~/assets/icons/QuestionMarkIcon"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { CodeBlock } from "~/components/code/CodeBlock"; import { InlineCode } from "~/components/code/InlineCode"; @@ -93,6 +94,7 @@ import { v3StandardTaskPath, v3TasksStreamingPath, v3TestTaskPath, + v3WebhookTaskPath, } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { @@ -153,6 +155,7 @@ const KIND_OPTIONS: { value: UnifiedTaskKind; label: string }[] = [ { value: "AGENT", label: "Agent" }, { value: "STANDARD", label: "Standard" }, { value: "SCHEDULED", label: "Scheduled" }, + { value: "WEBHOOK", label: "Webhook" }, ]; const VALID_KINDS = new Set(KIND_OPTIONS.map((o) => o.value)); @@ -182,6 +185,7 @@ const TASK_TYPE_SEGMENTS: { { value: "AGENT", tooltip: "Agent tasks", source: "AGENT" }, { value: "STANDARD", tooltip: "Standard tasks", source: "STANDARD" }, { value: "SCHEDULED", tooltip: "Scheduled tasks", source: "SCHEDULED" }, + { value: "WEBHOOK", tooltip: "Webhook tasks", source: "WEBHOOK" }, ]; const PAGE_SIZE = 25; @@ -410,12 +414,18 @@ function TaskRow({ ? v3AgentTaskPath(organization, project, environment, item.slug) : item.kind === "SCHEDULED" ? v3ScheduledTaskPath(organization, project, environment, item.slug) - : v3StandardTaskPath(organization, project, environment, item.slug); + : item.kind === "WEBHOOK" + ? v3WebhookTaskPath(organization, project, environment, item.slug) + : v3StandardTaskPath(organization, project, environment, item.slug); + // A webhook task runs from a verified inbound event, not a hand-built + // payload, so it has no Test page. const testPath = - item.kind === "AGENT" - ? v3PlaygroundAgentPath(organization, project, environment, item.slug) - : v3TestTaskPath(organization, project, environment, { taskIdentifier: item.slug }); + item.kind === "WEBHOOK" + ? undefined + : item.kind === "AGENT" + ? v3PlaygroundAgentPath(organization, project, environment, item.slug) + : v3TestTaskPath(organization, project, environment, { taskIdentifier: item.slug }); const runsPath = v3RunsPath(organization, project, environment, { tasks: [item.slug] }); @@ -434,7 +444,13 @@ function TaskRow({
- {item.kind === "AGENT" ? "Agent" : item.kind === "SCHEDULED" ? "Scheduled" : "Standard"} + {item.kind === "AGENT" + ? "Agent" + : item.kind === "SCHEDULED" + ? "Scheduled" + : item.kind === "WEBHOOK" + ? "Webhook" + : "Standard"} {item.kind === "AGENT" && item.agentType && ( {formatAgentType(item.agentType)} @@ -489,23 +505,27 @@ function TaskRow({ title="View runs" leadingIconClassName="-mx-1 text-runs" /> - + {testPath && ( + + )} } hiddenButtons={ - - Test - + testPath ? ( + + Test + + ) : undefined } /> @@ -783,6 +803,16 @@ function TaskTypeBreakdown() { you need.
+
+
+ + Webhook task +
+ + Runs from a verified inbound event sent to a hosted endpoint. Each delivery is logged, and + a successful one starts a run. + +
); } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx index d48a85f00fe..9c1499ec41d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/AIPayloadTabContent.tsx @@ -28,6 +28,8 @@ export function AIPayloadTabContent({ placeholder, examplePromptsOverride, isAgent = false, + payloadKind, + providerSource, showExamplePromptsHeader = true, }: { onPayloadGenerated: (payload: string) => void; @@ -38,6 +40,8 @@ export function AIPayloadTabContent({ placeholder?: string; examplePromptsOverride?: string[]; isAgent?: boolean; + payloadKind?: "standard" | "agent" | "webhook"; + providerSource?: string; showExamplePromptsHeader?: boolean; }) { const [prompt, setPrompt] = useState(""); @@ -79,6 +83,12 @@ export function AIPayloadTabContent({ formData.append("prompt", queryPrompt); formData.append("taskIdentifier", taskIdentifier); formData.append("isAgent", isAgent ? "true" : "false"); + if (payloadKind) { + formData.append("payloadKind", payloadKind); + } + if (providerSource) { + formData.append("providerSource", providerSource); + } if (payloadSchema) { formData.append("payloadSchema", JSON.stringify(payloadSchema)); } @@ -150,7 +160,15 @@ export function AIPayloadTabContent({ setIsLoading(false); } }, - [resourcePath, taskIdentifier, payloadSchema, getCurrentPayload, isAgent] + [ + resourcePath, + taskIdentifier, + payloadSchema, + getCurrentPayload, + isAgent, + payloadKind, + providerSource, + ] ); const processStreamEvent = useCallback( @@ -200,17 +218,23 @@ export function AIPayloadTabContent({ const examplePrompts = examplePromptsOverride ?? - (payloadSchema + (payloadKind === "webhook" ? [ - "Generate a valid payload", - "Generate a payload with edge cases", - "Generate a minimal payload with only required fields", + "Generate a realistic event body", + "Generate a failure/refund style event", + "Generate an event with nested data", ] - : [ - "Generate a simple JSON payload", - "Generate a payload with nested objects", - "Generate a payload with an array of items", - ]); + : payloadSchema + ? [ + "Generate a valid payload", + "Generate a payload with edge cases", + "Generate a minimal payload with only required fields", + ] + : [ + "Generate a simple JSON payload", + "Generate a payload with nested objects", + "Generate a payload with an array of items", + ]); return (
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 321d6391179..151d56534eb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -8,7 +8,12 @@ import { } from "@heroicons/react/20/solid"; import { DialogClose, DialogDescription } from "@radix-ui/react-dialog"; import { Form, useActionData, useFetcher, useParams, useSubmit } from "@remix-run/react"; -import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; +import { + type ActionFunction, + type LoaderFunctionArgs, + json, + redirect, +} from "@remix-run/server-runtime"; import { MachinePresetName } from "@trigger.dev/core/v3"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -47,6 +52,8 @@ import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource"; import { TimezoneList } from "~/components/scheduled/timezones"; import { $replica } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { redirectBackWithErrorMessage, @@ -124,8 +131,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }), ]); + if (result.foundTask && result.triggerSource === "WEBHOOK") { + throw redirect( + `/orgs/${organizationSlug}/projects/${projectParam}/env/${envParam}/webhooks/${taskParam}?tab=console` + ); + } + return typedjson({ ...result, regions: regionsResult.regions }); } catch (error) { + if (error instanceof Response) throw error; + logger.error("Failed to load test page", { taskParam, error: error instanceof Error ? error.message : error, @@ -291,7 +306,7 @@ export default function Page() { } }, [params.organizationSlug, params.projectParam, params.envParam]); - const defaultTaskQueue = result.queue; + const defaultTaskQueue = "queue" in result ? result.queue : undefined; const queues = useMemo(() => { const customQueues = queueFetcher.data?.queues ?? []; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx new file mode 100644 index 00000000000..2d65d81941c --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx @@ -0,0 +1,830 @@ +import { BookOpenIcon } from "@heroicons/react/24/solid"; +import { + Link, + type MetaFunction, + useNavigation, + useRevalidator, + useSearchParams, +} from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { type ReactNode, Suspense, useMemo, useState } from "react"; +import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; +import { z } from "zod"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; +import { PageBody } from "~/components/layout/AppLayout"; +import { DirectionSchema, ListPagination } from "~/components/ListPagination"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Card } from "~/components/primitives/charts/Card"; +import { Chart, type ChartConfig } from "~/components/primitives/charts/ChartCompound"; +import { CopyableText } from "~/components/primitives/CopyableText"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Header2 } from "~/components/primitives/Headers"; +import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; +import * as Property from "~/components/primitives/PropertyTable"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "~/components/primitives/Resizable"; +import { PulsingDot } from "~/components/primitives/PulsingDot"; +import { Spinner } from "~/components/primitives/Spinner"; +import { TabButton, TabContainer } from "~/components/primitives/Tabs"; +import { TimeFilter, timeFilterFromTo } from "~/components/runs/v3/SharedFilters"; +import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable"; +import { DeliveriesTable } from "~/components/webhookDeliveries/v1/DeliveriesTable"; +import { DeliveryStatusBadge } from "~/components/webhookDeliveries/v1/DeliveryStatus"; +import { EndpointsTable } from "~/components/webhookEndpoints/v1/EndpointsTable"; +import { WebhookComposer } from "~/components/webhookConsole/WebhookComposer"; +import { AIChatIcon } from "~/assets/icons/AIChatIcon"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; +import { $replica } from "~/db.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server"; +import { + WebhookDetailPresenter, + type WebhookActivity, + type WebhookDeliveriesList, + type WebhookDetail, +} from "~/presenters/v3/WebhookDetailPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { requireUser } from "~/services/session.server"; +import { + docsPath, + EnvironmentParamSchema, + v3EnvironmentPath, + v3RunPath, + v3SessionPath, + v3WebhookDeliveryPath, +} from "~/utils/pathBuilder"; +import { parseFiniteInt } from "~/utils/searchParams"; +import { useDeliveriesLiveReload } from "~/components/webhookDeliveries/v1/useDeliveriesLiveReload"; + +export const meta: MetaFunction = ({ data }) => { + const slug = (data as { webhook?: WebhookDetail | null } | undefined)?.webhook?.slug; + return [{ title: slug ? `${slug} | Webhooks | Trigger.dev` : "Webhook | Trigger.dev" }]; +}; + +const WebhookParamSchema = EnvironmentParamSchema.extend({ + webhookParam: z.string(), +}); + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const userId = user.id; + const { organizationSlug, projectParam, envParam, webhookParam } = + WebhookParamSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + throw new Response("Project not found", { status: 404 }); + } + + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) { + throw new Response("Environment not found", { status: 404 }); + } + + const url = new URL(request.url); + const period = url.searchParams.get("period") ?? undefined; + const from = parseFiniteInt(url.searchParams.get("from")); + const to = parseFiniteInt(url.searchParams.get("to")); + const cursor = url.searchParams.get("cursor") ?? undefined; + const directionRaw = url.searchParams.get("direction") ?? undefined; + const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined; + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + + const presenter = new WebhookDetailPresenter($replica, clickhouse); + const webhook = await presenter.findWebhook({ + environmentId: environment.id, + environmentType: environment.type, + webhookSlug: webhookParam, + }); + + if (!webhook) { + throw new Response("Webhook not found", { status: 404 }); + } + + const time = timeFilterFromTo({ period, from, to, defaultPeriod: "7d" }); + + const runActivity = presenter + .getRunActivity({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhookSlug: webhook.slug, + from: time.from, + to: time.to, + }) + .catch(() => ({ data: [], statuses: [] }) satisfies WebhookActivity); + + const deliveryActivity = presenter + .getDeliveryActivity({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhookEndpointId: webhook.endpoint.id, + from: time.from, + to: time.to, + }) + .catch(() => ({ data: [], statuses: [] }) satisfies WebhookActivity); + + const runList = new NextRunListPresenter($replica, clickhouse) + .call(project.organizationId, environment.id, { + userId, + projectId: project.id, + tasks: [webhook.slug], + period, + from, + to, + cursor, + direction, + }) + .catch(() => null); + + const deliveriesList = presenter + .listDeliveries({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhookEndpointId: webhook.endpoint.id, + period, + from, + to, + cursor, + direction, + }) + .catch(() => null); + + const endpointsList = presenter + .listEndpoints({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + handlerWebhookId: webhook.slug, + }) + .catch(() => [] as Awaited>); + + const composerEndpoints = presenter + .listComposerEndpoints({ environmentId: environment.id, handlerWebhookId: webhook.slug }) + .catch(() => [] as Awaited>); + + return typeddefer({ + webhook, + runActivity, + deliveryActivity, + runList, + deliveriesList, + endpointsList, + composerEndpoints, + }); +}; + +type WebhookTab = "runs" | "deliveries" | "endpoints" | "console"; + +export default function Page() { + const { + webhook, + runActivity, + deliveryActivity, + runList, + deliveriesList, + endpointsList, + composerEndpoints, + } = useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + const tasksPath = v3EnvironmentPath(organization, project, environment); + + const [searchParams] = useSearchParams(); + const [tab, setTab] = useState(() => { + const requested = searchParams.get("tab"); + return requested === "console" || requested === "runs" || requested === "endpoints" + ? requested + : "deliveries"; + }); + const tabLabel = + tab === "deliveries" + ? "Deliveries" + : tab === "runs" + ? "Runs" + : tab === "console" + ? "Console" + : "Endpoints"; + + return ( + <> + + + + {webhook.slug} + + } + /> + + + Webhooks docs + + + + + + +
+ {/* Top bar: tabs on the left, TimeFilter + pagination on the right. + h-10 matches the right-hand sidebar header height. */} +
+ + setTab("deliveries")} + > + Deliveries + + setTab("runs")} + > + Runs + + setTab("endpoints")} + > + Endpoints + + setTab("console")} + > + Console + + + {tab !== "endpoints" && tab !== "console" && ( +
+ + {tab === "deliveries" ? ( + + + {(list) => (list ? : null)} + + + ) : ( + + + {(list) => (list ? : null)} + + + )} +
+ )} +
+ + {tab === "endpoints" ? ( + // Endpoints aren't a time series, so no activity chart or time filter. +
+ }> + }> + {(endpoints) => ( + + )} + + +
+ ) : tab === "console" ? ( +
+ }> + }> + {(endpoints) => + endpoints.length === 0 ? ( +
+ This webhook has no synced endpoints to send to yet. +
+ ) : ( + + ) + } +
+
+
+ ) : ( + + {/* Activity chart (one status-bucket chart per tab). */} + +
+
+ + {tab === "deliveries" ? ( + }> + } + > + {(result) => } + + + ) : ( + }> + } + > + {(result) => } + + + )} + +
+
+
+ + + + {/* Table */} + + + +
+ )} +
+
+ + + + {tab === "console" ? ( + + ) : ( + setTab("endpoints")} /> + )} + +
+
+ + ); +} + +type LoaderData = ReturnType>; + +function WebhookContentArea({ + tab, + deliveriesList, + runList, + webhookEndpointId, +}: { + tab: WebhookTab; + webhookEndpointId: string; +} & Pick) { + return ( +
+ {tab === "deliveries" ? ( + }> + }> + {(list) => + list ? ( +
+ +
+ ) : ( + + ) + } +
+
+ ) : ( + }> + }> + {(list) => + list ? ( +
+ +
+ ) : ( + + ) + } +
+
+ )} +
+ ); +} + +function LiveDeliveriesTable({ + list, + webhookEndpointId, +}: { + list: WebhookDeliveriesList; + webhookEndpointId: string; +}) { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const navigation = useNavigation(); + const revalidator = useRevalidator(); + const [searchParams, setSearchParams] = useSearchParams(); + + const { visibleDeliveries, showNewDeliveriesBanner, newDeliveriesCount, dismissNewDeliveries } = + useDeliveriesLiveReload({ + deliveries: list.deliveries, + hasAnyDeliveries: list.deliveries.length > 0, + isLoading: navigation.state !== "idle", + webhookEndpointId, + organizationSlug: organization.slug, + projectSlug: project.slug, + environmentSlug: environment.slug, + }); + + const onClickShowNewDeliveries = () => { + dismissNewDeliveries(); + if (searchParams.has("cursor") || searchParams.has("direction")) { + setSearchParams((prev) => { + prev.delete("cursor"); + prev.delete("direction"); + return prev; + }); + return; + } + revalidator.revalidate(); + }; + + return ( + <> + {showNewDeliveriesBanner ? ( +
+ + + +
+ ) : null} + + + ); +} + +function ConsoleLiveFeed({ + deliveriesList, + webhookEndpointId, +}: { + webhookEndpointId: string; +} & Pick) { + return ( +
+
+ Live deliveries + + + Live + +
+
+ }> + }> + {(list) => + list ? ( + + ) : ( + + ) + } + + +
+
+ ); +} + +function ConsoleFeedList({ + list, + webhookEndpointId, +}: { + list: WebhookDeliveriesList; + webhookEndpointId: string; +}) { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const navigation = useNavigation(); + const revalidator = useRevalidator(); + + const { visibleDeliveries, showNewDeliveriesBanner, newDeliveriesCount, dismissNewDeliveries } = + useDeliveriesLiveReload({ + deliveries: list.deliveries, + hasAnyDeliveries: list.deliveries.length > 0, + isLoading: navigation.state !== "idle", + webhookEndpointId, + organizationSlug: organization.slug, + projectSlug: project.slug, + environmentSlug: environment.slug, + }); + + const onShowNew = () => { + dismissNewDeliveries(); + revalidator.revalidate(); + }; + + if (visibleDeliveries.length === 0) { + return ( +

+ No deliveries yet. Send an event to watch it arrive here. +

+ ); + } + + return ( +
+ {showNewDeliveriesBanner ? ( + + ) : null} + {visibleDeliveries.map((delivery) => ( + +
+ + {delivery.friendlyId} + {delivery.isTest ? ( + + Test + + ) : null} + + +
+ {delivery.session ? ( + + + {delivery.session.friendlyId} + + ) : delivery.run ? ( + + + {delivery.run.friendlyId} + + ) : null} + + ))} +
+ ); +} + +function WebhookDetailSidebar({ + webhook, + onViewEndpoints, +}: { + webhook: WebhookDetail; + onViewEndpoints: () => void; +}) { + return ( +
+
+ + + {webhook.slug} + +
+
+ + + Source + + {webhook.source} + + + + Endpoints + + {/* The connect flow (ingress URL, secret, provider setup) lives on each + endpoint, so this handler view points there instead of holding it. */} + + + + + File path + + + + + + Created + + + + + +
+
+ ); +} + +const STATUS_COLOR: Record = { + // Run statuses + COMPLETED: "#28BF5C", + RUNNING: "#3B82F6", + FAILED: "#E11D48", + CANCELED: "#878C99", + // Delivery statuses + SUCCEEDED: "#28BF5C", + PROCESSING: "#3B82F6", + PENDING: "#878C99", +}; + +function ActivityChart({ activity }: { activity: WebhookActivity }) { + const chartConfig: ChartConfig = useMemo(() => { + const cfg: ChartConfig = {}; + for (const status of activity.statuses) { + cfg[status] = { + label: status.charAt(0) + status.slice(1).toLowerCase(), + color: STATUS_COLOR[status] ?? "#9CA3AF", + }; + } + return cfg; + }, [activity.statuses]); + + const { xAxisFormatter, xAxisTicks, tooltipLabelFormatter } = useMemo( + () => buildTimeAxis(activity.data), + [activity.data] + ); + + return ( + + + + ); +} + +function ActivityChartSkeleton() { + return ( +
+ {Array.from({ length: 42 }).map((_, i) => ( +
+ ))} +
+ ); +} + +function TableLoading() { + return ( +
+ +
+ ); +} + +function ChartCard({ title, children }: { title: string; children: ReactNode }) { + return ( + + {title} +
{children}
+
+ ); +} + +function buildTimeAxis(data: WebhookActivity["data"]) { + const range = data.length >= 2 ? data[data.length - 1].bucket - data[0].bucket : 0; + const oneDay = 24 * 60 * 60 * 1000; + const showTime = range <= oneDay; + + const xAxisFormatter = (value: number) => { + const date = new Date(value); + return showTime + ? date.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + timeZone: "UTC", + }) + : date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + timeZone: "UTC", + }); + }; + + const xAxisTicks = showTime + ? undefined + : data.filter((d) => new Date(d.bucket).getUTCHours() === 0).map((d) => d.bucket); + + const bucketMs = data.length >= 2 ? data[1].bucket - data[0].bucket : 0; + const isSubDayBucket = bucketMs > 0 && bucketMs < oneDay; + + const tooltipLabelFormatter = (_label: string, payload: { payload?: { bucket?: number } }[]) => { + const ts = payload?.[0]?.payload?.bucket; + if (typeof ts !== "number" || !Number.isFinite(ts)) return _label; + const date = new Date(ts); + return isSubDayBucket + ? date.toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + timeZone: "UTC", + }) + : date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "UTC", + }); + }; + + return { xAxisFormatter, xAxisTicks, tooltipLabelFormatter }; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks._index/route.tsx new file mode 100644 index 00000000000..c8eee2552c7 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks._index/route.tsx @@ -0,0 +1,239 @@ +import { + type MetaFunction, + useNavigation, + useRevalidator, + useSearchParams, +} from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { WebhookDeliveryStatus } from "@trigger.dev/database"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { PageBody } from "~/components/layout/AppLayout"; +import { DirectionSchema, ListPagination } from "~/components/ListPagination"; +import { Button } from "~/components/primitives/Buttons"; +import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; +import { PulsingDot } from "~/components/primitives/PulsingDot"; +import { DeliveriesTable } from "~/components/webhookDeliveries/v1/DeliveriesTable"; +import { useDeliveriesLiveReload } from "~/components/webhookDeliveries/v1/useDeliveriesLiveReload"; +import { + type PossibleWebhook, + WebhookDeliveryFilters, +} from "~/components/webhookDeliveries/v1/WebhookDeliveryFilters"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { type WebhookDeliveryListItem } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { $replica, webhookReplica } from "~/db.server"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { WebhookDeliveriesListPresenter } from "~/presenters/v3/WebhookDeliveriesListPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { requireUser } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { parseFiniteInt } from "~/utils/searchParams"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; + +const VALID_DELIVERY_STATUSES = new Set(Object.values(WebhookDeliveryStatus)); + +// Accepts repeated `statuses` params or a single CSV value; drops anything that +// isn't one of the four WebhookDeliveryStatus values. +function parseStatuses(searchParams: URLSearchParams): WebhookDeliveryStatus[] | undefined { + const raw = searchParams + .getAll("statuses") + .flatMap((value) => value.split(",")) + .map((value) => value.trim()) + .filter((value) => VALID_DELIVERY_STATUSES.has(value)) as WebhookDeliveryStatus[]; + + return raw.length > 0 ? Array.from(new Set(raw)) : undefined; +} + +function parseRepeated(searchParams: URLSearchParams, key: string): string[] | undefined { + const values = searchParams.getAll(key).filter((value) => value.length > 0); + return values.length > 0 ? values : undefined; +} + +export const meta: MetaFunction = () => [{ title: "Deliveries | Webhooks | Trigger.dev" }]; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const userId = user.id; + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) throw new Response("Project not found", { status: 404 }); + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) throw new Response("Environment not found", { status: 404 }); + + // Feature gate: enabled by a global FeatureFlag OR a per-org override; admins/impersonators + // always pass. flag() resolves org override -> global -> default(false). + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) throw new Response("Not found", { status: 404 }); + } + + const url = new URL(request.url); + const periodParam = url.searchParams.get("period") ?? undefined; + const from = parseFiniteInt(url.searchParams.get("from")); + const to = parseFiniteInt(url.searchParams.get("to")); + const cursor = url.searchParams.get("cursor") ?? undefined; + const directionRaw = url.searchParams.get("direction") ?? undefined; + const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined; + + const statuses = parseStatuses(url.searchParams); + const webhooks = parseRepeated(url.searchParams, "webhooks"); + const deliveryId = url.searchParams.get("deliveryId") ?? undefined; + const runId = url.searchParams.get("runId") ?? undefined; + const testParam = url.searchParams.get("test"); + const isTest = testParam === "only" ? true : testParam === "hide" ? false : undefined; + + // Default to the last 7 days when no explicit window is set, matching the TimeFilter default. + const hasExplicitWindow = Boolean(periodParam || from || to); + const period = periodParam ?? (hasExplicitWindow ? undefined : "7d"); + + const hasFilters = Boolean( + statuses || webhooks || deliveryId || runId || testParam || hasExplicitWindow + ); + + // Distinct handler slugs (one row per handlerWebhookId) for the Webhook picker. + const endpoints = await webhookReplica.webhookEndpoint.findMany({ + where: { runtimeEnvironmentId: environment.id }, + select: { handlerWebhookId: true, source: true }, + distinct: ["handlerWebhookId"], + }); + const possibleWebhooks: PossibleWebhook[] = endpoints.map((e) => ({ + slug: e.handlerWebhookId, + source: e.source, + })); + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + + const presenter = new WebhookDeliveriesListPresenter($replica, clickhouse); + const list = await presenter + .call({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhooks, + statuses, + deliveryId, + runId, + isTest, + period, + from, + to, + cursor, + direction, + }) + .catch(() => ({ deliveries: [], pagination: {} })); + + return typedjson({ + deliveries: list.deliveries, + pagination: list.pagination, + possibleWebhooks, + hasFilters, + }); +}; + +export default function Page() { + const { deliveries, pagination, possibleWebhooks, hasFilters } = + useTypedLoaderData(); + + return ( + <> + + + + +
+
+ + +
+ +
+
+ + ); +} + +function LiveDeliveriesTable({ + deliveries, + hasFilters, +}: { + deliveries: WebhookDeliveryListItem[]; + hasFilters: boolean; +}) { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const navigation = useNavigation(); + const revalidator = useRevalidator(); + const [searchParams, setSearchParams] = useSearchParams(); + + const { visibleDeliveries, showNewDeliveriesBanner, newDeliveriesCount, dismissNewDeliveries } = + useDeliveriesLiveReload({ + deliveries, + hasAnyDeliveries: deliveries.length > 0, + isLoading: navigation.state !== "idle", + organizationSlug: organization.slug, + projectSlug: project.slug, + environmentSlug: environment.slug, + }); + + const onClickShowNewDeliveries = () => { + dismissNewDeliveries(); + if (searchParams.has("cursor") || searchParams.has("direction")) { + setSearchParams((prev) => { + prev.delete("cursor"); + prev.delete("direction"); + return prev; + }); + return; + } + revalidator.revalidate(); + }; + + return ( +
+ {showNewDeliveriesBanner ? ( +
+ + + +
+ ) : null} +
+ +
+
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.$deliveryParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.$deliveryParam/route.tsx new file mode 100644 index 00000000000..303a54b5035 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.$deliveryParam/route.tsx @@ -0,0 +1,406 @@ +import { BookOpenIcon } from "@heroicons/react/24/solid"; +import { type MetaFunction, useRevalidator } from "@remix-run/react"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { useState } from "react"; +import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { z } from "zod"; +import { AIChatIcon } from "~/assets/icons/AIChatIcon"; +import { RunsIcon } from "~/assets/icons/RunsIcon"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; +import { CodeBlock } from "~/components/code/CodeBlock"; +import { PageBody } from "~/components/layout/AppLayout"; +import { LinkButton } from "~/components/primitives/Buttons"; +import { CopyableText } from "~/components/primitives/CopyableText"; +import { TextLink } from "~/components/primitives/TextLink"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Header2 } from "~/components/primitives/Headers"; +import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import * as Property from "~/components/primitives/PropertyTable"; +import { TabButton, TabContainer } from "~/components/primitives/Tabs"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "~/components/primitives/Resizable"; +import { DeliveryStatusBadge } from "~/components/webhookDeliveries/v1/DeliveryStatus"; +import { DeliveryTimeline } from "~/components/webhookDeliveries/v1/DeliveryTimeline"; +import { $replica } from "~/db.server"; +import { env } from "~/env.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useInterval } from "~/hooks/useInterval"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { + WebhookDeliveryDetailPresenter, + type WebhookDeliveryDetail, +} from "~/presenters/v3/WebhookDeliveryDetailPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { requireUser } from "~/services/session.server"; +import { + docsPath, + EnvironmentParamSchema, + v3RunPath, + v3SessionPath, + v3WebhooksPath, + v3WebhookTaskPath, +} from "~/utils/pathBuilder"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; + +const DeliveryParamSchema = EnvironmentParamSchema.extend({ + deliveryParam: z.string(), +}); + +export const meta: MetaFunction = ({ data }) => { + const friendlyId = (data as { delivery?: WebhookDeliveryDetail } | undefined)?.delivery + ?.friendlyId; + return [ + { title: friendlyId ? `${friendlyId} | Deliveries | Trigger.dev` : "Delivery | Trigger.dev" }, + ]; +}; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const user = await requireUser(request); + const userId = user.id; + const { organizationSlug, projectParam, envParam, deliveryParam } = + DeliveryParamSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) throw new Response("Project not found", { status: 404 }); + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) throw new Response("Environment not found", { status: 404 }); + + // Feature gate: same as the deliveries list. Global FeatureFlag OR per-org override; + // admins/impersonators always pass. + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) throw new Response("Not found", { status: 404 }); + } + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + + const presenter = new WebhookDeliveryDetailPresenter($replica, clickhouse); + const delivery = await presenter.call({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + deliveryFriendlyId: deliveryParam, + }); + + // A missing delivery is most often one that aged out of retention (a bookmarked or shared link), + // so render a friendly retention-aware state rather than a hard 404. + return typedjson({ delivery, retentionDays: env.WEBHOOK_PARTITION_RETENTION_DAYS }); +}; + +function formatDuration(createdAt: Date, processedAt: Date | null): string | null { + if (!processedAt) return null; + const ms = processedAt.getTime() - createdAt.getTime(); + if (ms < 0) return null; + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +export default function Page() { + const { delivery, retentionDays } = useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const revalidator = useRevalidator(); + + const inFlight = delivery?.status === "PENDING" || delivery?.status === "PROCESSING"; + useInterval({ + interval: 3000, + pauseWhenHidden: true, + disabled: !inFlight, + callback: () => revalidator.revalidate(), + }); + + const deliveriesPath = v3WebhooksPath(organization, project, environment); + + if (!delivery) { + return ( + <> + + + + +
+ + Delivery not available + + This delivery couldn't be found. Deliveries are retained for {retentionDays} days, so + it may have aged out. + + + Back to deliveries + +
+
+ + ); + } + + const runPath = delivery.run + ? v3RunPath(organization, project, environment, { friendlyId: delivery.run.friendlyId }) + : undefined; + const sessionPath = delivery.session + ? v3SessionPath(organization, project, environment, { friendlyId: delivery.session.friendlyId }) + : undefined; + const webhookPath = delivery.webhook + ? v3WebhookTaskPath(organization, project, environment, delivery.webhook.slug) + : undefined; + + const eventJson = + delivery.parsedEvent != null ? JSON.stringify(delivery.parsedEvent, null, 2) : null; + const headersJson = + delivery.headers != null && Object.keys(delivery.headers as object).length > 0 + ? JSON.stringify(delivery.headers, null, 2) + : null; + const duration = formatDuration(delivery.createdAt, delivery.processedAt); + + const [tab, setTab] = useState<"event" | "headers">("event"); + + return ( + <> + + + + {delivery.friendlyId} + + + } + /> + + + Webhooks docs + + + + + + +
+
+ + setTab("event")} + > + Event payload + + setTab("headers")} + > + Request headers + + +
+
+ {tab === "event" ? ( + eventJson ? ( + + ) : ( + + No event payload was captured for this delivery. + + ) + ) : headersJson ? ( + + ) : ( + + No request headers were captured for this delivery. + + )} +
+
+
+ + + +
+
+ Delivery +
+
+ + + + ID + + + + + + Status + + + + + {delivery.filterReason ? ( + + Filter reason + + {delivery.filterReason} + + + ) : null} + + Webhook + + {delivery.webhook ? ( + webhookPath ? ( + + + {delivery.webhook.slug} + + ) : ( + + + {delivery.webhook.slug} + + ) + ) : ( + Unknown + )} + + + {delivery.session && sessionPath ? ( + + Session + + + + {delivery.session.friendlyId} + + + + ) : null} + + Run + + {delivery.run && runPath ? ( + + + {delivery.run.friendlyId} + + ) : ( + None + )} + + + + External delivery ID + + {delivery.externalDeliveryId ? ( + + ) : ( + None + )} + + + + Idempotency key + + {delivery.idempotencyKey ? ( + + ) : ( + None + )} + + + + Raw body hash + + {delivery.rawBodyHash ? ( + + ) : ( + None + )} + + + + Created + + + + + + Processed + + {delivery.processedAt ? ( + + ) : ( + None + )} + + + {duration ? ( + + Duration + {duration} + + ) : null} + {delivery.status === "FAILED" && delivery.errorMessage ? ( + + Error + + {delivery.errorMessage} + + + ) : null} + +
+
+
+
+
+ + ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx new file mode 100644 index 00000000000..ae011d6a091 --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx @@ -0,0 +1,732 @@ +import { BookOpenIcon, KeyIcon, SparklesIcon } from "@heroicons/react/24/solid"; +import { useFetcher, type MetaFunction } from "@remix-run/react"; +import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { randomBytes } from "node:crypto"; +import { WebhookRoutingTarget, WebhookVerifierArtifact } from "@trigger.dev/core/v3"; +import type { WebhookValueSource } from "@trigger.dev/core/v3"; +import { useEffect, useState } from "react"; +import { Suspense } from "react"; +import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; +import { z } from "zod"; +import { WebhookIcon } from "~/assets/icons/WebhookIcon"; +import { CodeBlock } from "~/components/code/CodeBlock"; +import { PageBody } from "~/components/layout/AppLayout"; +import { DirectionSchema, ListPagination } from "~/components/ListPagination"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { ClipboardField } from "~/components/primitives/ClipboardField"; +import { CopyableText } from "~/components/primitives/CopyableText"; +import { DateTime } from "~/components/primitives/DateTime"; +import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; +import { Header2, Header3 } from "~/components/primitives/Headers"; +import { Hint } from "~/components/primitives/Hint"; +import { Input } from "~/components/primitives/Input"; +import { Label } from "~/components/primitives/Label"; +import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import * as Property from "~/components/primitives/PropertyTable"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup, +} from "~/components/primitives/Resizable"; +import { Spinner } from "~/components/primitives/Spinner"; +import { TextLink } from "~/components/primitives/TextLink"; +import { TimeFilter } from "~/components/runs/v3/SharedFilters"; +import { DeliveriesTable } from "~/components/webhookDeliveries/v1/DeliveriesTable"; +import { EndpointStatusBadge } from "~/components/webhookEndpoints/v1/EndpointStatus"; +import { $replica, prisma, webhookPrisma } from "~/db.server"; +import { webhookIngressUrl } from "~/utils/webhookIngressUrl.server"; +import { useEnvironment } from "~/hooks/useEnvironment"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { + WebhookDetailPresenter, + type WebhookEndpointDetail, +} from "~/presenters/v3/WebhookDetailPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { getSecretStore } from "~/services/secrets/secretStore.server"; +import { requireUser } from "~/services/session.server"; +import { + docsPath, + EnvironmentParamSchema, + v3WebhookTaskPath, + v3WebhooksPath, +} from "~/utils/pathBuilder"; +import { parseFiniteInt } from "~/utils/searchParams"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; + +const EndpointParamSchema = EnvironmentParamSchema.extend({ + endpointParam: z.string(), +}); + +export const meta: MetaFunction = ({ data }) => { + const friendlyId = (data as { endpoint?: WebhookEndpointDetail } | undefined)?.endpoint + ?.friendlyId; + return [ + { title: friendlyId ? `${friendlyId} | Endpoints | Trigger.dev` : "Endpoint | Trigger.dev" }, + ]; +}; + +// Shared gate + scope resolution for the loader and action. +async function requireWebhookAccess(request: Request, params: LoaderFunctionArgs["params"]) { + const user = await requireUser(request); + const { organizationSlug, projectParam, envParam, endpointParam } = + EndpointParamSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, user.id); + if (!project) throw new Response("Project not found", { status: 404 }); + const environment = await findEnvironmentBySlug(project.id, envParam, user.id); + if (!environment) throw new Response("Environment not found", { status: 404 }); + + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) throw new Response("Not found", { status: 404 }); + } + + return { user, project, environment, endpointParam }; +} + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const { user, project, environment, endpointParam } = await requireWebhookAccess(request, params); + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + const presenter = new WebhookDetailPresenter($replica, clickhouse); + + const endpoint = await presenter.findEndpoint({ + environmentId: environment.id, + endpointFriendlyId: endpointParam, + }); + if (!endpoint) throw new Response("Endpoint not found", { status: 404 }); + + const ingestUrl = webhookIngressUrl(endpoint.opaqueId); + + // Parse the tagged-union JSON columns for display (engine validates on write). + const routing = WebhookRoutingTarget.safeParse(endpoint.routingTarget); + const verifier = WebhookVerifierArtifact.safeParse(endpoint.verifierArtifact); + + const url = new URL(request.url); + const period = url.searchParams.get("period") ?? undefined; + const from = parseFiniteInt(url.searchParams.get("from")); + const to = parseFiniteInt(url.searchParams.get("to")); + const cursor = url.searchParams.get("cursor") ?? undefined; + const directionRaw = url.searchParams.get("direction") ?? undefined; + const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined; + + const deliveriesList = presenter + .listDeliveries({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhookEndpointId: endpoint.id, + period, + from, + to, + cursor, + direction, + }) + .catch(() => null); + + return typeddefer({ + endpoint, + ingestUrl, + routing: routing.success ? routing.data : null, + verifier: verifier.success ? verifier.data : null, + deliveriesList, + }); +}; + +const SetSecretSchema = z.object({ + intent: z.literal("set-secret"), + secret: z.string().trim().min(1, "A signing secret is required"), +}); + +export const action = async ({ request, params }: ActionFunctionArgs) => { + const { project, environment, endpointParam } = await requireWebhookAccess(request, params); + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + const presenter = new WebhookDetailPresenter($replica, clickhouse); + const endpoint = await presenter.findEndpoint({ + environmentId: environment.id, + endpointFriendlyId: endpointParam, + }); + if (!endpoint) throw new Response("Endpoint not found", { status: 404 }); + + // Store the plaintext secret encrypted under the DATABASE SecretStore provider, in the exact + // shape the engine's resolveSigningSecret reads ({ secret }). Key is namespaced by the endpoint's + // internal id; point signingSecretKey at it so verification picks it up. + const secretKey = `webhook:signing-secret:${endpoint.id}`; + const secretStore = getSecretStore("DATABASE", { prismaClient: prisma }); + + const formData = await request.formData(); + const intent = formData.get("intent"); + + // Generate (integrator-supplied secret): mint a strong secret, store it, and return it so the + // UI can reveal it ONCE for the integrator to paste into their provider. + if (intent === "generate-secret") { + const secret = `whsec_${randomBytes(32).toString("hex")}`; + await secretStore.setSecret(secretKey, { secret }); + await webhookPrisma.webhookEndpoint.update({ + where: { id: endpoint.id }, + data: { signingSecretKey: secretKey }, + }); + return { success: true as const, generatedSecret: secret }; + } + + // Set/Rotate (paste a provider-supplied secret). + const submission = SetSecretSchema.safeParse(Object.fromEntries(formData)); + if (!submission.success) { + return { success: false as const, error: submission.error.issues[0]?.message ?? "Invalid" }; + } + await secretStore.setSecret(secretKey, { secret: submission.data.secret }); + await webhookPrisma.webhookEndpoint.update({ + where: { id: endpoint.id }, + data: { signingSecretKey: secretKey }, + }); + + return { success: true as const }; +}; + +export default function Page() { + const { endpoint, ingestUrl, routing, verifier, deliveriesList } = + useTypedLoaderData(); + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + + const handlerPath = v3WebhookTaskPath( + organization, + project, + environment, + endpoint.handlerWebhookId + ); + + return ( + <> + + + + {endpoint.friendlyId} + + + } + /> + + + Webhooks docs + + + + + + +
+
+ Deliveries +
+ + + + {(list) => (list ? : null)} + + +
+
+
+ }> + }> + {(list) => + list ? ( + + ) : ( + + ) + } + + +
+
+
+ + + + + +
+
+ + ); +} + +type LoaderData = ReturnType>; + +function EndpointSidebar({ + endpoint, + ingestUrl, + routing, + verifier, + handlerPath, +}: { + endpoint: WebhookEndpointDetail; + ingestUrl: string; + routing: LoaderData["routing"]; + verifier: LoaderData["verifier"]; + handlerPath: string; +}) { + const metadataJson = + endpoint.metadata != null && Object.keys(endpoint.metadata as object).length > 0 + ? JSON.stringify(endpoint.metadata, null, 2) + : null; + + // Asymmetric endpoints store the provider's PUBLIC KEY, not a shared signing secret. + const scheme = verifier && verifier.kind !== "bundle" ? verifier.config.scheme : undefined; + const credentialNoun = scheme === "asymmetric" ? "public key" : "signing secret"; + const credentialLabel = scheme === "asymmetric" ? "Public key" : "Signing secret"; + + // Generate-and-reveal makes sense when the integrator chooses the secret (and it's an HMAC + // shared secret, not a provider public key). "provider" endpoints only paste. + const canGenerate = + scheme !== "asymmetric" && + (endpoint.secretProvisioning === "integrator" || endpoint.secretProvisioning === "either"); + + return ( +
+
+ + + {endpoint.friendlyId} + +
+
+ {/* Connect: the important new bit. Everything an integrator needs to point a provider here. */} +
+ Connect + + + Webhook URL + + + + + + {credentialLabel} + +
+ {endpoint.hasSigningSecret ? ( + + + Set + + ) : ( + Not set, all deliveries are rejected + )} +
+ {canGenerate ? ( + + ) : null} + +
+
+
+
+
+ +
+ +
+ Routing + + + Target + + {routing?.type === "task" ? ( + + {routing.taskId} + + ) : routing?.type === "session" ? ( + session: {routing.taskIdentifier} + ) : ( + Unknown + )} + + + +
+ +
+ Scope + + + Source + + {endpoint.source} + + + + Tenant + + {endpoint.isDefault ? ( + default + ) : ( + {endpoint.tenantId} + )} + + + + External ref + + {endpoint.externalRef ? ( + {endpoint.externalRef} + ) : ( + None + )} + + + + Status + + + + + + Created + + + + + + Updated + + + + + +
+ + {metadataJson ? ( +
+ Metadata + +
+ ) : null} +
+
+ ); +} + +function ProviderSetup({ verifier, source }: { verifier: LoaderData["verifier"]; source: string }) { + if (!verifier) return null; + + if (verifier.kind === "bundle") { + return ( + + This endpoint uses a custom verifier bundle. + + ); + } + + const presetName = verifier.kind === "preset" ? verifier.preset : null; + const config = verifier.config; + + return ( +
+ + Provider setup + + + + Scheme + + + {presetName ? `${presetName} (${config.scheme})` : config.scheme} + + + + {config.scheme === "hmac" || config.scheme === "asymmetric" ? ( + <> + + Signature header + + {config.signatureHeader} + + + + Algorithm + + + {config.algorithm} / {config.encoding} + + + + {config.timestamp ? ( + + Timestamp + + + {describeTimestampSource(config.timestamp.source)} + + + + ) : null} + + Signing string + + + {config.signingString === "raw" ? "raw body" : config.signingString.template} + + + + {config.scheme === "asymmetric" ? ( + + Public key + + {config.publicKeyEncoding ?? "pem"} + + + ) : null} + + ) : config.scheme === "shared-secret" ? ( + <> + + Placement + + {config.placement} + + + {config.fieldName ? ( + + Field name + + {config.fieldName} + + + ) : null} + + ) : ( + <> + + Placement + + {config.placement} + + + + Param name + + {config.paramName} + + + + )} + + + {config.scheme === "asymmetric" + ? `${source} signs with its private key; set its public key above.` + : `Sign deliveries with the ${source} scheme above, using the signing secret.`} + +
+ ); +} + +// Human-readable description of where the replay timestamp is read from. +function describeTimestampSource(source: WebhookValueSource): string { + switch (source.from) { + case "header": + return `header ${source.name}`; + case "signatureField": + return `field "${source.field}" in signature header`; + case "body": + return `body ${source.path}`; + case "url": + return "request URL"; + case "constant": + return "constant"; + } +} + +function SetSecretDialog({ + hasSigningSecret, + credentialNoun, + variant = "secondary/small", +}: { + hasSigningSecret: boolean; + credentialNoun: string; + variant?: "secondary/small" | "tertiary/small"; +}) { + const fetcher = useFetcher(); + const [open, setOpen] = useState(false); + const isSubmitting = fetcher.state !== "idle"; + const verb = hasSigningSecret ? "Rotate" : "Set"; + const isPublicKey = credentialNoun === "public key"; + + // Close on a successful save; the loader revalidates and the state flips to "Set". + useEffect(() => { + if (fetcher.state === "idle" && fetcher.data?.success) { + setOpen(false); + } + }, [fetcher.state, fetcher.data]); + + return ( + + + + + + + {verb} {credentialNoun} + + + +
+ + + + {isPublicKey + ? "The provider's public key. Stored encrypted; deliveries are verified against it." + : "Stored encrypted and never shown again. Deliveries are verified against this secret."} + +
+ {fetcher.data && !fetcher.data.success ? ( + + {fetcher.data.error} + + ) : null} +
+ + +
+
+
+
+ ); +} + +// For integrator-supplied secrets (GitHub/GitLab/standard): mint a strong secret server-side, +// store it, and reveal it ONCE so the user can paste it into their provider. +function GenerateSecretDialog({ hasSigningSecret }: { hasSigningSecret: boolean }) { + const fetcher = useFetcher(); + const [open, setOpen] = useState(false); + const isSubmitting = fetcher.state !== "idle"; + const generated = + fetcher.data && "generatedSecret" in fetcher.data ? fetcher.data.generatedSecret : undefined; + + return ( + + + + + + + {hasSigningSecret ? "Regenerate signing secret" : "Generate signing secret"} + + {generated ? ( +
+ + Copy this now. It won't be shown again. + + + Paste this into your provider's webhook signing-secret field. +
+ +
+
+ ) : ( + + + + Trigger.dev generates a strong signing secret, stores it encrypted, and shows it once + so you can paste it into your provider. + +
+ + +
+
+ )} +
+
+ ); +} + +function TableLoading() { + return ( +
+ +
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks/route.tsx new file mode 100644 index 00000000000..f6723ddebaa --- /dev/null +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks/route.tsx @@ -0,0 +1,10 @@ +import { Outlet } from "@remix-run/react"; +import { PageContainer } from "~/components/layout/AppLayout"; + +export default function Page() { + return ( + + + + ); +} diff --git a/apps/webapp/app/routes/api.v1.sessions.ts b/apps/webapp/app/routes/api.v1.sessions.ts index 1a10e6c6f02..86e49772bb0 100644 --- a/apps/webapp/app/routes/api.v1.sessions.ts +++ b/apps/webapp/app/routes/api.v1.sessions.ts @@ -7,8 +7,7 @@ import { type SessionItem, type SessionStatus, } from "@trigger.dev/core/v3"; -import { SessionId } from "@trigger.dev/core/v3/isomorphic"; -import type { Prisma, Session } from "@trigger.dev/database"; +import type { Session } from "@trigger.dev/database"; import { $replica, prisma, type PrismaClient } from "~/db.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { logger } from "~/services/logger.server"; @@ -17,8 +16,8 @@ import { ensureRunForSession, type SessionTriggerConfig, } from "~/services/realtime/sessionRunManager.server"; -import { chatSnapshotStoragePathForSession } from "~/services/realtime/chatSnapshot.server"; import { + findOrCreateSession, serializeSession, serializeSessionsWithFriendlyRunIds, } from "~/services/realtime/sessions.server"; @@ -164,66 +163,18 @@ const { action } = createActionApiRoute( }, async ({ authentication, body }) => { try { - const { id, friendlyId } = SessionId.generate(); - - // Idempotent on (env, externalId): two concurrent POSTs converge - // to the same row. We refresh `triggerConfig` on the cached path - // so newly-deployed schema changes (e.g. an updated - // `clientDataSchema` on the agent) propagate to subsequent runs - // — the next `ensureRunForSession` reads back the latest config. - let session: Session; - let isCached = false; - - const triggerConfigJson = body.triggerConfig as unknown as Prisma.InputJsonValue; - - if (body.externalId) { - session = await prisma.session.upsert({ - where: { - runtimeEnvironmentId_externalId: { - runtimeEnvironmentId: authentication.environment.id, - externalId: body.externalId, - }, - }, - create: { - id, - friendlyId, - externalId: body.externalId, - type: body.type, - taskIdentifier: body.taskIdentifier, - triggerConfig: triggerConfigJson, - tags: body.tags ?? [], - metadata: body.metadata as Prisma.InputJsonValue | undefined, - expiresAt: body.expiresAt ?? null, - projectId: authentication.environment.projectId, - runtimeEnvironmentId: authentication.environment.id, - environmentType: authentication.environment.type, - organizationId: authentication.environment.organizationId, - streamBasinName: authentication.environment.organization.streamBasinName, - chatSnapshotStoragePath: chatSnapshotStoragePathForSession(friendlyId), - }, - update: { triggerConfig: triggerConfigJson }, - }); - isCached = session.id !== id; - } else { - session = await prisma.session.create({ - data: { - id, - friendlyId, - type: body.type, - taskIdentifier: body.taskIdentifier, - triggerConfig: triggerConfigJson, - tags: body.tags ?? [], - metadata: body.metadata as Prisma.InputJsonValue | undefined, - expiresAt: body.expiresAt ?? null, - projectId: authentication.environment.projectId, - runtimeEnvironmentId: authentication.environment.id, - environmentType: authentication.environment.type, - organizationId: authentication.environment.organizationId, - streamBasinName: authentication.environment.organization.streamBasinName, - chatSnapshotStoragePath: chatSnapshotStoragePathForSession(friendlyId), - }, - }); - } + // Idempotent on (env, externalId): two concurrent POSTs converge to the same row, and + // `triggerConfig` is refreshed on the cached path so a redeployed config reaches the next run. + const { session, isCached } = await findOrCreateSession({ + environment: authentication.environment, + externalId: body.externalId, + type: body.type, + taskIdentifier: body.taskIdentifier, + triggerConfig: body.triggerConfig, + tags: body.tags, + metadata: body.metadata as Record | undefined, + expiresAt: body.expiresAt, + }); // Reject create on a closed session. The upsert path will return // an already-closed row when the caller reuses an externalId, and diff --git a/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts b/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts new file mode 100644 index 00000000000..0b375acb4d2 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts @@ -0,0 +1,58 @@ +import { json } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { webhookReplica } from "~/db.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; +import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; +import { webhookEngine } from "~/v3/webhookEngine.server"; + +const ParamsSchema = z.object({ deliveryId: z.string() }); + +// POST /api/v1/webhooks/deliveries/:deliveryId/replay — re-run the delivery's task from its stored +// event as a NEW delivery (we don't keep the raw body, so this re-triggers rather than re-verifies). +export const { action, loader } = createActionApiRoute( + { + params: ParamsSchema, + method: "POST", + allowJWT: true, + corsStrategy: "all", + authorization: { action: "write", resource: () => ({ type: "webhooks" }) }, + }, + async ({ params, authentication }) => { + const env = authentication.environment; + + // Resolve the friendly id to the internal (id, createdAt) the engine needs (ClickHouse -> PG). + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + env.organizationId, + "standard" + ); + const original = await webhookDeliveriesRepository({ + clickhouse, + prisma: webhookReplica, + }).getDelivery({ + organizationId: env.organizationId, + projectId: env.project.id, + environmentId: env.id, + friendlyId: params.deliveryId, + }); + if (!original) return json({ error: "Not found" }, { status: 404 }); + + const result = await webhookEngine.replayDelivery({ + id: original.id, + createdAt: original.createdAt, + }); + + switch (result.outcome) { + case "replayed": + return json({ deliveryId: result.deliveryFriendlyId, replayedFrom: params.deliveryId }); + case "delivery_not_found": + case "endpoint_not_found": + return json({ error: "Not found" }, { status: 404 }); + case "unsupported_target": + return json( + { error: "This delivery's endpoint does not route to a task." }, + { status: 400 } + ); + } + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.ts b/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.ts new file mode 100644 index 00000000000..b54a01e11e1 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.ts @@ -0,0 +1,25 @@ +import { json } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { findWebhookDeliveryResource } from "~/presenters/v3/ApiWebhookDeliveryPresenter.server"; +import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; + +const ParamsSchema = z.object({ + deliveryId: z.string(), +}); + +export const loader = createLoaderApiRoute( + { + params: ParamsSchema, + allowJWT: true, + corsStrategy: "all", + findResource: async (params, authentication) => + findWebhookDeliveryResource(authentication, params.deliveryId), + authorization: { + action: "read", + resource: () => ({ type: "webhooks" }), + }, + }, + async ({ resource }) => { + return json(resource); + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.deliveries.ts b/apps/webapp/app/routes/api.v1.webhooks.deliveries.ts new file mode 100644 index 00000000000..3607f6b30e1 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.deliveries.ts @@ -0,0 +1,22 @@ +import { json } from "@remix-run/server-runtime"; +import { + ApiWebhookDeliveryListPresenter, + ApiWebhookDeliveryListSearchParams, +} from "~/presenters/v3/ApiWebhookDeliveryPresenter.server"; +import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; + +export const loader = createLoaderApiRoute( + { + searchParams: ApiWebhookDeliveryListSearchParams, + findResource: async () => 1, // Collection route — nothing to resolve. + allowJWT: true, + corsStrategy: "all", + authorization: { action: "read", resource: () => ({ type: "webhooks" }) }, + }, + async ({ searchParams, authentication }) => { + const presenter = new ApiWebhookDeliveryListPresenter(); + const result = await presenter.call(authentication.environment, searchParams); + + return json(result); + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts new file mode 100644 index 00000000000..545f434122e --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts @@ -0,0 +1,34 @@ +import { json } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { webhookPrisma } from "~/db.server"; +import { findWebhookEndpointResource } from "~/presenters/v3/ApiWebhookEndpointPresenter.server"; +import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; +import { webhookEngine } from "~/v3/webhookEngine.server"; + +const ParamsSchema = z.object({ endpointId: z.string() }); + +// POST /api/v1/webhooks/endpoints/:endpointId/disable — pause an endpoint (ingress returns 404). +export const { action, loader } = createActionApiRoute( + { + params: ParamsSchema, + method: "POST", + allowJWT: true, + corsStrategy: "all", + authorization: { action: "write", resource: () => ({ type: "webhooks" }) }, + }, + async ({ params, authentication }) => { + const env = authentication.environment; + const endpoint = await webhookPrisma.webhookEndpoint.findFirst({ + where: { friendlyId: params.endpointId, runtimeEnvironmentId: env.id }, + }); + if (!endpoint) return json({ error: "Not found" }, { status: 404 }); + + await webhookPrisma.webhookEndpoint.update({ + where: { id: endpoint.id }, + data: { status: "INACTIVE" }, + }); + webhookEngine.invalidateEndpoint(endpoint.opaqueId); + + return json(await findWebhookEndpointResource(authentication, params.endpointId)); + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts new file mode 100644 index 00000000000..562a3a80fb6 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts @@ -0,0 +1,34 @@ +import { json } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { webhookPrisma } from "~/db.server"; +import { findWebhookEndpointResource } from "~/presenters/v3/ApiWebhookEndpointPresenter.server"; +import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; +import { webhookEngine } from "~/v3/webhookEngine.server"; + +const ParamsSchema = z.object({ endpointId: z.string() }); + +// POST /api/v1/webhooks/endpoints/:endpointId/enable — resume a paused endpoint. +export const { action, loader } = createActionApiRoute( + { + params: ParamsSchema, + method: "POST", + allowJWT: true, + corsStrategy: "all", + authorization: { action: "write", resource: () => ({ type: "webhooks" }) }, + }, + async ({ params, authentication }) => { + const env = authentication.environment; + const endpoint = await webhookPrisma.webhookEndpoint.findFirst({ + where: { friendlyId: params.endpointId, runtimeEnvironmentId: env.id }, + }); + if (!endpoint) return json({ error: "Not found" }, { status: 404 }); + + await webhookPrisma.webhookEndpoint.update({ + where: { id: endpoint.id }, + data: { status: "ACTIVE" }, + }); + webhookEngine.invalidateEndpoint(endpoint.opaqueId); + + return json(await findWebhookEndpointResource(authentication, params.endpointId)); + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts new file mode 100644 index 00000000000..caaf9c6b7a6 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts @@ -0,0 +1,51 @@ +import { json } from "@remix-run/server-runtime"; +import { randomBytes } from "node:crypto"; +import { WebhookVerifierArtifact } from "@trigger.dev/core/v3"; +import { z } from "zod"; +import { prisma, webhookPrisma } from "~/db.server"; +import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server"; +import { getSecretStore } from "~/services/secrets/secretStore.server"; +import { webhookEngine } from "~/v3/webhookEngine.server"; + +const ParamsSchema = z.object({ endpointId: z.string() }); + +// POST /api/v1/webhooks/endpoints/:endpointId/rotate-secret — mint a new signing secret and return +// it ONCE. Only for schemes we generate (hmac / shared-secret); asymmetric endpoints set a public key. +export const { action, loader } = createActionApiRoute( + { + params: ParamsSchema, + method: "POST", + allowJWT: true, + corsStrategy: "all", + authorization: { action: "write", resource: () => ({ type: "webhooks" }) }, + }, + async ({ params, authentication }) => { + const env = authentication.environment; + const endpoint = await webhookPrisma.webhookEndpoint.findFirst({ + where: { friendlyId: params.endpointId, runtimeEnvironmentId: env.id }, + }); + if (!endpoint) return json({ error: "Not found" }, { status: 404 }); + + const parsed = WebhookVerifierArtifact.safeParse(endpoint.verifierArtifact); + if (parsed.success && "config" in parsed.data && parsed.data.config.scheme === "asymmetric") { + return json( + { + error: "Cannot generate a secret for an asymmetric endpoint; set its public key instead.", + }, + { status: 400 } + ); + } + + const secret = `whsec_${randomBytes(32).toString("hex")}`; + const secretKey = `webhook:signing-secret:${endpoint.id}`; + await getSecretStore("DATABASE", { prismaClient: prisma }).setSecret(secretKey, { secret }); + await webhookPrisma.webhookEndpoint.update({ + where: { id: endpoint.id }, + data: { signingSecretKey: secretKey }, + }); + // Drop the cached secret so verification uses the new one immediately on this instance. + webhookEngine.invalidateEndpoint(endpoint.opaqueId); + + return json({ id: endpoint.friendlyId, secretSet: true as const, secret }); + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.ts new file mode 100644 index 00000000000..f4a308f1056 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.ts @@ -0,0 +1,25 @@ +import { json } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { findWebhookEndpointResource } from "~/presenters/v3/ApiWebhookEndpointPresenter.server"; +import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; + +const ParamsSchema = z.object({ + endpointId: z.string(), +}); + +export const loader = createLoaderApiRoute( + { + params: ParamsSchema, + allowJWT: true, + corsStrategy: "all", + findResource: async (params, authentication) => + findWebhookEndpointResource(authentication, params.endpointId), + authorization: { + action: "read", + resource: () => ({ type: "webhooks" }), + }, + }, + async ({ resource }) => { + return json(resource); + } +); diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.ts new file mode 100644 index 00000000000..11d9d7d3d73 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.ts @@ -0,0 +1,22 @@ +import { json } from "@remix-run/server-runtime"; +import { + ApiWebhookEndpointListPresenter, + ApiWebhookEndpointListSearchParams, +} from "~/presenters/v3/ApiWebhookEndpointPresenter.server"; +import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; + +export const loader = createLoaderApiRoute( + { + searchParams: ApiWebhookEndpointListSearchParams, + findResource: async () => 1, // Collection route — nothing to resolve. + allowJWT: true, + corsStrategy: "all", + authorization: { action: "read", resource: () => ({ type: "webhooks" }) }, + }, + async ({ searchParams, authentication }) => { + const presenter = new ApiWebhookEndpointListPresenter(); + const result = await presenter.call(authentication.environment, searchParams); + + return json(result); + } +); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.ai-generate-payload.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.ai-generate-payload.tsx index d03b1985cb4..7053520854c 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.ai-generate-payload.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.ai-generate-payload.tsx @@ -22,6 +22,8 @@ const RequestSchema = z.object({ payloadSchema: z.string().max(50_000).optional(), currentPayload: z.string().max(50_000).optional(), isAgent: z.enum(["true", "false"]).optional(), + payloadKind: z.enum(["standard", "agent", "webhook"]).optional(), + providerSource: z.string().max(100).optional(), }); export async function action({ request, params }: ActionFunctionArgs) { @@ -65,20 +67,31 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } - const { prompt, taskIdentifier, payloadSchema, currentPayload, isAgent } = submission.data; - const agentMode = isAgent === "true"; + const { + prompt, + taskIdentifier, + payloadSchema, + currentPayload, + isAgent, + payloadKind, + providerSource, + } = submission.data; + const kind = payloadKind ?? (isAgent === "true" ? "agent" : "standard"); logger.info("[AI payload] Generating payload", { taskIdentifier, hasPayloadSchema: !!payloadSchema, hasCurrentPayload: !!currentPayload, promptLength: prompt.length, - agentMode, + kind, }); - const systemPrompt = agentMode - ? buildAgentClientDataPrompt(taskIdentifier, payloadSchema, currentPayload) - : buildSystemPrompt(taskIdentifier, payloadSchema, currentPayload); + const systemPrompt = + kind === "webhook" + ? buildWebhookEventPrompt(providerSource, currentPayload) + : kind === "agent" + ? buildAgentClientDataPrompt(taskIdentifier, payloadSchema, currentPayload) + : buildSystemPrompt(taskIdentifier, payloadSchema, currentPayload); const stream = new ReadableStream({ async start(controller) { @@ -101,16 +114,19 @@ export async function action({ request, params }: ActionFunctionArgs) { abortSignal: getRequestAbortSignal(), system: systemPrompt, prompt, - tools: { - getTaskSourceCode: tool({ - description: - "Look up the source code of the task to understand what payload shape it expects. Use this when there is no JSON Schema available and you need to infer the payload structure from the task implementation.", - inputSchema: z.object({}), - execute: async () => { - return getTaskSourceCode(environment.id, environment.type, taskIdentifier); - }, - }), - }, + tools: + kind === "webhook" + ? {} + : { + getTaskSourceCode: tool({ + description: + "Look up the source code of the task to understand what payload shape it expects. Use this when there is no JSON Schema available and you need to infer the payload structure from the task implementation.", + inputSchema: z.object({}), + execute: async () => { + return getTaskSourceCode(environment.id, environment.type, taskIdentifier); + }, + }), + }, stopWhen: stepCountIs(3), }); @@ -293,6 +309,43 @@ Use this as context but generate new client data based on the user's prompt.`; return prompt; } +function buildWebhookEventPrompt(providerSource?: string, currentPayload?: string): string { + const provider = providerSource && providerSource !== "custom" ? providerSource : undefined; + + let prompt = `You are generating a realistic example webhook event body${ + provider ? ` for the "${provider}" provider` : "" + }. + +Return ONLY the raw JSON event body that the provider would POST to a webhook endpoint, wrapped in a \`\`\`json code block. Do NOT include HTTP headers, a signature, or any {event, headers} envelope — just the event JSON itself. + +Requirements: +- Generate a realistic, well-formed event payload matching the shape ${ + provider ? `the "${provider}" provider actually sends` : "a typical webhook provider sends" + }. +- Use plausible values (real-looking ids with the provider's prefixes, unix/ISO timestamps, emails, amounts). +- Include the fields a consumer branches on (e.g. an event "type"/"event" discriminator and a "data"/"object" payload) when the provider uses them. +- The JSON must be valid and parseable.`; + + if (provider) { + prompt += ` + +If you know the "${provider}" webhook event schema, follow it closely (field names, nesting, id prefixes) and pick a common, representative event type for this provider.`; + } + + if (currentPayload) { + prompt += ` + +The current event body in the editor is: +\`\`\`json +${currentPayload} +\`\`\` + +Use it as context but generate a new event based on the user's prompt.`; + } + + return prompt; +} + function buildSystemPrompt( taskIdentifier: string, payloadSchema?: string, diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts new file mode 100644 index 00000000000..f6f529aa668 --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts @@ -0,0 +1,122 @@ +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { typedjson } from "remix-typedjson"; +import { z } from "zod"; +import { $replica, webhookReplica } from "~/db.server"; +import { resolveDeliveryRunTargets } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { loadProjectEnvironmentFromRequest } from "~/services/loadProjectEnvironmentFromRequest.server"; +import { + type ListedWebhookDelivery, + webhookDeliveriesRepository, +} from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; + +const deliveryIdsQueryParam = z + .string() + .optional() + .transform((value) => { + const ids = + value + ?.split(",") + .map((id) => id.trim()) + .filter(Boolean) ?? []; + return [...new Set(ids)].slice(0, 100); + }); + +const SearchParamsSchema = z.object({ + webhookEndpointId: z.string().optional(), + deliveryIds: deliveryIdsQueryParam, + includeNewDeliveries: z + .string() + .optional() + .transform((value) => value === "true"), + since: z.coerce.number().optional(), + to: z.coerce.number().optional(), +}); + +export type LiveDeliveryFields = { + friendlyId: string; + status: ListedWebhookDelivery["status"]; + runId: string | null; + run: { friendlyId: string } | null; + session: { friendlyId: string; externalId: string | null } | null; + errorMessage: string | null; + processedAt: Date | null; +}; + +export function mapDeliveryToLiveFields( + delivery: ListedWebhookDelivery, + targets: { + runFriendlyIdById: Map; + sessionByRunId: Map; + } +): LiveDeliveryFields { + const runFriendlyId = delivery.runId ? targets.runFriendlyIdById.get(delivery.runId) : undefined; + return { + friendlyId: delivery.friendlyId, + status: delivery.status, + runId: delivery.runId, + run: runFriendlyId ? { friendlyId: runFriendlyId } : null, + session: delivery.runId ? (targets.sessionByRunId.get(delivery.runId) ?? null) : null, + errorMessage: delivery.errorMessage, + processedAt: delivery.processedAt, + }; +} + +export async function loader({ request, params }: LoaderFunctionArgs) { + const url = new URL(request.url); + const { webhookEndpointId, deliveryIds, includeNewDeliveries, since, to } = + SearchParamsSchema.parse(Object.fromEntries(url.searchParams)); + + const newDeliveriesSince = includeNewDeliveries && since !== undefined ? since : undefined; + + if (deliveryIds.length === 0 && newDeliveriesSince === undefined) { + return typedjson({ deliveries: [] }); + } + + const { project, environment } = await loadProjectEnvironmentFromRequest(request, params); + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + const repository = webhookDeliveriesRepository({ clickhouse, prisma: webhookReplica }); + + const [deliveries, newDeliveriesResult] = await Promise.all([ + deliveryIds.length > 0 + ? repository + .getDeliveriesByFriendlyIds({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + friendlyIds: deliveryIds, + }) + .then(async (rows) => { + const targets = await resolveDeliveryRunTargets($replica, rows); + return rows.map((row) => mapDeliveryToLiveFields(row, targets)); + }) + : Promise.resolve([]), + newDeliveriesSince !== undefined + ? (async () => { + if (to !== undefined && to <= newDeliveriesSince) { + return { count: 0, since: newDeliveriesSince }; + } + const { deliveryIds: newIds } = await repository.listDeliveryIds({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhookEndpointId, + from: newDeliveriesSince + 1, + to, + page: { size: 100 }, + }); + return { count: newIds.length, since: newDeliveriesSince }; + })() + : Promise.resolve(undefined), + ]); + + if (newDeliveriesResult) { + return typedjson({ deliveries, ...newDeliveriesResult }); + } + + return typedjson({ deliveries }); +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.replay-source.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.replay-source.ts new file mode 100644 index 00000000000..54305c31adc --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.replay-source.ts @@ -0,0 +1,148 @@ +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { WebhookVerifierArtifact } from "@trigger.dev/core/v3"; +import { typedjson } from "remix-typedjson"; +import { z } from "zod"; +import { $replica, webhookReplica } from "~/db.server"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { WebhookDetailPresenter } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { requireUser } from "~/services/session.server"; +import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; + +const ParamsSchema = EnvironmentParamSchema.extend({ endpointParam: z.string() }); + +const RECENT_DELIVERIES_LIMIT = 20; + +export type ReplaySourceDelivery = { + friendlyId: string; + status: string; + isTest: boolean; + externalDeliveryId: string; + createdAt: string; +}; + +export type ReplaySourceData = + | { kind: "list"; deliveries: ReplaySourceDelivery[] } + | { kind: "payload"; body: string; headers: Record } + | { kind: "error"; error: string }; + +const HEADER_DENYLIST = new Set([ + "content-type", + "content-length", + "host", + "accept", + "accept-encoding", + "accept-language", + "user-agent", + "connection", + "cache-control", + "pragma", + "origin", + "referer", + "x-trigger-test", +]); + +function isNoiseHeader(lower: string): boolean { + return lower.startsWith("sec-") || HEADER_DENYLIST.has(lower); +} + +function signatureHeaderNames(artifact: unknown): Set { + const names = new Set(); + const parsed = WebhookVerifierArtifact.safeParse(artifact); + if (!parsed.success || parsed.data.kind === "bundle") return names; + const config = parsed.data.config; + if (config.scheme === "hmac" || config.scheme === "asymmetric") { + names.add(config.signatureHeader.toLowerCase()); + if (config.timestamp?.source.from === "header") { + names.add(config.timestamp.source.name.toLowerCase()); + } + } + return names; +} + +export async function loader({ request, params }: LoaderFunctionArgs) { + const user = await requireUser(request); + const { organizationSlug, projectParam, envParam, endpointParam } = ParamsSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, user.id); + if (!project) return typedjson({ kind: "error", error: "Project not found" } as ReplaySourceData); + const environment = await findEnvironmentBySlug(project.id, envParam, user.id); + if (!environment) + return typedjson({ kind: "error", error: "Environment not found" } as ReplaySourceData); + + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) return typedjson({ kind: "error", error: "Not found" } as ReplaySourceData); + } + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + const presenter = new WebhookDetailPresenter($replica, clickhouse); + const endpoint = await presenter.findEndpoint({ + environmentId: environment.id, + endpointFriendlyId: endpointParam, + }); + if (!endpoint) + return typedjson({ kind: "error", error: "Endpoint not found" } as ReplaySourceData); + + const repository = webhookDeliveriesRepository({ clickhouse, prisma: webhookReplica }); + + const url = new URL(request.url); + const deliveryId = url.searchParams.get("deliveryId") ?? undefined; + + if (deliveryId) { + const delivery = await repository.getDelivery({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + friendlyId: deliveryId, + }); + if (!delivery || delivery.webhookEndpointId !== endpoint.id) { + return typedjson({ kind: "error", error: "Delivery not found" } as ReplaySourceData); + } + const body = + delivery.parsedEvent != null ? JSON.stringify(delivery.parsedEvent, null, 2) : "{}"; + const strip = signatureHeaderNames(endpoint.verifierArtifact); + const rawHeaders = (delivery.headers ?? {}) as Record; + const headers: Record = {}; + for (const [key, value] of Object.entries(rawHeaders)) { + const lower = key.toLowerCase(); + if (isNoiseHeader(lower) || strip.has(lower)) continue; + if (typeof value === "string") headers[key] = value; + } + return typedjson({ kind: "payload", body, headers } as ReplaySourceData); + } + + const { deliveries } = await repository.listDeliveries({ + organizationId: project.organizationId, + projectId: project.id, + environmentId: environment.id, + webhookEndpointId: endpoint.id, + page: { size: RECENT_DELIVERIES_LIMIT }, + }); + + return typedjson({ + kind: "list", + deliveries: deliveries.map((delivery) => ({ + friendlyId: delivery.friendlyId, + status: delivery.status, + isTest: delivery.isTest, + externalDeliveryId: delivery.externalDeliveryId, + createdAt: delivery.createdAt.toISOString(), + })), + } as ReplaySourceData); +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.send.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.send.ts new file mode 100644 index 00000000000..f43f83464c5 --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam.send.ts @@ -0,0 +1,217 @@ +import { signWithVerifierConfig } from "@internal/webhook-engine"; +import { type ActionFunctionArgs, redirect } from "@remix-run/server-runtime"; +import { WebhookVerifierArtifact } from "@trigger.dev/core/v3"; +import { z } from "zod"; +import { $replica, prisma } from "~/db.server"; +import { env } from "~/env.server"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { WebhookDetailPresenter } from "~/presenters/v3/WebhookDetailPresenter.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { getSecretStore } from "~/services/secrets/secretStore.server"; +import { requireUser } from "~/services/session.server"; +import { webhookConsoleSendRateLimiter } from "~/services/webhookConsoleSendRateLimit.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { webhookIngressUrl } from "~/utils/webhookIngressUrl.server"; +import { webhookEngine } from "~/v3/webhookEngine.server"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; + +const ParamsSchema = EnvironmentParamSchema.extend({ endpointParam: z.string() }); + +const SigningSecretSchema = z.object({ secret: z.string() }); + +const SendSchema = z.object({ + body: z.string(), + headers: z.record(z.string()).optional(), + signatureMode: z.enum(["signed", "unsigned", "tampered", "simulate"]).default("signed"), + redirect: z.boolean().default(true), +}); + +export type WebhookSendResult = + | { + success: true; + httpStatus: number; + deliveryId?: string; + deduplicated?: boolean; + handshake?: boolean; + responseBody: string; + } + | { success: false; error: string; notSignable?: boolean }; + +/** + * Authenticated dashboard test-send. Session/cookie auth via requireUser; authorization via + * findProjectBySlug (org membership) + findEnvironmentBySlug (env access) + the webhooks feature flag. + * The delivery is injected in-process through the engine (verify -> filter -> route -> run), never by + * looping back through the public ingress, so it does not consume the provider's ingress rate budget + * and is not gated by WEBHOOK_INGRESS_ENABLED. + */ +export async function action({ request, params }: ActionFunctionArgs): Promise { + const user = await requireUser(request); + const { organizationSlug, projectParam, envParam, endpointParam } = ParamsSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, user.id); + if (!project) return { success: false, error: "Project not found" }; + const environment = await findEnvironmentBySlug(project.id, envParam, user.id); + if (!environment) return { success: false, error: "Environment not found" }; + + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) return { success: false, error: "Not found" }; + } + + const rateLimit = await webhookConsoleSendRateLimiter.limit(user.id); + if (!rateLimit.success) { + return { success: false, error: "Too many test sends. Wait a moment and try again." }; + } + + const parsed = SendSchema.safeParse(await request.json()); + if (!parsed.success) { + return { success: false, error: parsed.error.issues[0]?.message ?? "Invalid request" }; + } + const { body, headers: providedHeaders, signatureMode, redirect: shouldRedirect } = parsed.data; + + const rawBody = new TextEncoder().encode(body); + const limitBytes = env.WEBHOOK_INGRESS_BODY_SIZE_LIMIT_MB * 1024 * 1024; + if (rawBody.length > limitBytes) { + return { + success: false, + error: `Body exceeds the ${env.WEBHOOK_INGRESS_BODY_SIZE_LIMIT_MB} MB limit.`, + }; + } + + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( + project.organizationId, + "standard" + ); + const presenter = new WebhookDetailPresenter($replica, clickhouse); + const endpoint = await presenter.findEndpoint({ + environmentId: environment.id, + endpointFriendlyId: endpointParam, + }); + if (!endpoint) return { success: false, error: "Endpoint not found" }; + + const verifier = WebhookVerifierArtifact.safeParse(endpoint.verifierArtifact); + if (!verifier.success) return { success: false, error: "Endpoint has no verifier config" }; + if (verifier.data.kind === "bundle") { + return { success: false, error: "Bundle verifiers cannot be sent from the console" }; + } + const config = verifier.data.config; + + const ingressUrl = webhookIngressUrl(endpoint.opaqueId); + const baseHeaders = { ...(providedHeaders ?? {}) }; + + let ingestHeaders: Record = baseHeaders; + let ingestBody: Uint8Array = rawBody; + let ingestUrl = ingressUrl; + + if (signatureMode === "signed") { + if (!endpoint.hasSigningSecret) { + return { success: false, error: "This endpoint has no signing secret. Set one first." }; + } + const secretStore = getSecretStore("DATABASE", { prismaClient: prisma }); + const stored = await secretStore.getSecret( + SigningSecretSchema, + `webhook:signing-secret:${endpoint.id}` + ); + if (!stored?.secret) { + return { success: false, error: "The signing secret could not be read." }; + } + const signed = signWithVerifierConfig({ + config, + secret: stored.secret, + rawBody, + url: ingressUrl, + headers: baseHeaders, + }); + if (!signed.ok) { + return { success: false, error: signed.error, notSignable: signed.notSignable }; + } + ingestHeaders = signed.headers; + ingestBody = signed.body; + ingestUrl = signed.url; + } else if (signatureMode === "tampered") { + const bogus = signWithVerifierConfig({ + config, + secret: `tampered-${Date.now()}`, + rawBody, + url: ingressUrl, + headers: baseHeaders, + }); + if (bogus.ok) { + ingestHeaders = bogus.headers; + ingestBody = bogus.body; + ingestUrl = bogus.url; + } else if (config.scheme === "hmac" || config.scheme === "asymmetric") { + ingestHeaders = { ...baseHeaders, [config.signatureHeader]: "deadbeef" }; + } + } + + const finalHeaders: Record = { + "content-type": "application/json", + ...ingestHeaders, + "x-trigger-test": "1", + }; + + const ingestInput = { + opaqueId: endpoint.opaqueId, + rawBytes: ingestBody, + headers: finalHeaders, + url: ingestUrl, + }; + const result = + signatureMode === "simulate" + ? await webhookEngine.simulateInject(ingestInput) + : await webhookEngine.ingest(ingestInput); + + const deliveryPathFor = (friendlyId: string) => + `/orgs/${organizationSlug}/projects/${projectParam}/env/${envParam}/webhooks/deliveries/${friendlyId}`; + + switch (result.outcome) { + case "accepted": { + const friendlyId = result.deliveryFriendlyId; + if (shouldRedirect) throw redirect(deliveryPathFor(friendlyId)); + return { + success: true, + httpStatus: 200, + deliveryId: friendlyId, + responseBody: JSON.stringify({ received: true, deliveryId: friendlyId }), + }; + } + case "handshake": + return { success: true, httpStatus: 200, handshake: true, responseBody: result.body }; + case "duplicate": { + const friendlyId = `whd_${result.deliveryId}`; + if (shouldRedirect) throw redirect(deliveryPathFor(friendlyId)); + return { + success: true, + httpStatus: 200, + deliveryId: friendlyId, + deduplicated: true, + responseBody: JSON.stringify({ received: true, deliveryId: friendlyId }), + }; + } + case "verification_failed": + return { + success: false, + error: result.error ?? "Signature verification failed. No delivery was recorded.", + }; + case "secret_missing": + return { success: false, error: "This endpoint has no signing secret. Set one first." }; + case "endpoint_not_found": + case "endpoint_inactive": + return { success: false, error: "This endpoint is not active." }; + case "enqueue_failed": + return { success: false, error: result.error ?? "Failed to record the delivery." }; + default: + return { success: false, error: "The delivery could not be sent." }; + } +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts new file mode 100644 index 00000000000..0b253e1be4f --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts @@ -0,0 +1,106 @@ +import { + categoryLabel, + categoryOrder, + getProvider, + getSample, + sampleManifest, +} from "@internal/webhook-sources"; +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { typedjson } from "remix-typedjson"; +import { findProjectBySlug } from "~/models/project.server"; +import { requireUser } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; + +export type WebhookSampleMeta = { + provider: string; + providerLabel?: string; + presetId?: string; + eventType: string; + name: string; + description?: string; +}; + +/** Provider metadata joined from the registry, for the producer pane. */ +export type WebhookProviderMeta = { + id: string; + label: string; + category?: string; + categoryLabel?: string; + docsUrl?: string; + /** Set when the provider maps to a verifier preset (i.e. round-trippable / first-class). */ + preset?: string; + eventCount: number; +}; + +export type WebhookSamplesData = + | { kind: "manifest"; providers: WebhookProviderMeta[]; samples: WebhookSampleMeta[] } + | { kind: "body"; body: string; extraHeaders?: Record } + | { kind: "error"; error: string }; + +function titleCase(id: string): string { + return id.charAt(0).toUpperCase() + id.slice(1); +} + +export async function loader({ request, params }: LoaderFunctionArgs) { + const user = await requireUser(request); + const { organizationSlug, projectParam } = EnvironmentParamSchema.parse(params); + + const project = await findProjectBySlug(organizationSlug, projectParam, user.id); + if (!project) + return typedjson({ kind: "error", error: "Project not found" } as WebhookSamplesData); + + const url = new URL(request.url); + const provider = url.searchParams.get("provider") ?? undefined; + const eventType = url.searchParams.get("eventType") ?? undefined; + + if (provider && eventType) { + const sample = getSample(provider, eventType); + if (!sample) + return typedjson({ kind: "error", error: "Sample not found" } as WebhookSamplesData); + return typedjson({ + kind: "body", + body: JSON.stringify(sample.body, null, 2), + extraHeaders: sample.extraHeaders, + } as WebhookSamplesData); + } + + const manifest = sampleManifest().filter((sample) => getProvider(sample.provider)); + + const items: WebhookSampleMeta[] = manifest.map((sample) => { + const entry = getProvider(sample.provider); + return { + provider: sample.provider, + providerLabel: entry?.label ?? sample.providerLabel, + presetId: sample.presetId, + eventType: sample.eventType, + name: sample.name, + description: sample.description, + }; + }); + + const providerMap = new Map(); + for (const sample of manifest) { + const existing = providerMap.get(sample.provider); + if (existing) { + existing.eventCount += 1; + continue; + } + const entry = getProvider(sample.provider); + providerMap.set(sample.provider, { + id: sample.provider, + label: entry?.label ?? sample.providerLabel ?? titleCase(sample.provider), + category: entry?.category, + categoryLabel: entry ? categoryLabel(entry.category) : undefined, + docsUrl: entry?.docsUrl, + preset: entry?.preset, + eventCount: 1, + }); + } + + const providers = [...providerMap.values()].sort((a, b) => { + const cat = categoryOrder(a.category ?? "") - categoryOrder(b.category ?? ""); + return cat !== 0 ? cat : a.label.localeCompare(b.label); + }); + + return typedjson({ kind: "manifest", providers, samples: items } as WebhookSamplesData); +} diff --git a/apps/webapp/app/routes/webhooks.v1.ingest.$opaqueId.ts b/apps/webapp/app/routes/webhooks.v1.ingest.$opaqueId.ts new file mode 100644 index 00000000000..125d127f3c8 --- /dev/null +++ b/apps/webapp/app/routes/webhooks.v1.ingest.$opaqueId.ts @@ -0,0 +1,75 @@ +import { type ActionFunctionArgs, json } from "@remix-run/server-runtime"; +import { env } from "~/env.server"; +import { logger } from "~/services/logger.server"; +import { readBodyWithCap } from "~/utils/readBodyWithCap.server"; +import { webhookIngressRateLimiter } from "~/services/webhookIngressRateLimit.server"; +import { webhookEngine } from "~/v3/webhookEngine.server"; + +// Public, unauthenticated webhook ingress. A Remix `action` (NOT +// createActionApiRoute, which parses JSON) so we can capture the raw bytes the +// signature scheme verifies. The engine resolves the endpoint (and its env id + +// type) from the globally-unique opaqueId, so this route runs no env query. +export async function action({ request, params }: ActionFunctionArgs) { + if (request.method !== "POST") { + return json({ error: "Method not allowed" }, { status: 405 }); + } + if (env.WEBHOOK_INGRESS_ENABLED !== "1") { + return json({ error: "Not found" }, { status: 404 }); + } + + const opaqueId = params.opaqueId; + if (!opaqueId) return json({ error: "Not found" }, { status: 404 }); + + // Per-opaqueId rate limit FIRST, before any DB or secret work. + const rl = await webhookIngressRateLimiter.limit(opaqueId); + if (!rl.success) { + logger.info("webhook ingress rate limited", { opaqueId }); + return json({ error: "Too many requests" }, { status: 429 }); + } + + // Content-Length is a cheap fast-path reject; the capped streaming read is the real enforcement + // (a chunked request can omit/understate Content-Length and would otherwise buffer unbounded). + const limitBytes = env.WEBHOOK_INGRESS_BODY_SIZE_LIMIT_MB * 1024 * 1024; + const contentLength = Number(request.headers.get("content-length") ?? "0"); + if (Number.isFinite(contentLength) && contentLength > limitBytes) { + return json({ error: "Payload too large" }, { status: 413 }); + } + + const rawBytes = await readBodyWithCap(request, limitBytes); + if (rawBytes === null) { + return json({ error: "Payload too large" }, { status: 413 }); + } + + const headers: Record = {}; + request.headers.forEach((v, k) => (headers[k] = v)); + + const result = await webhookEngine.ingest({ + opaqueId, + rawBytes, + headers, + url: request.url, // url-secret reads this; never logged with its query string + }); + + switch (result.outcome) { + case "accepted": + logger.info("webhook ingress accepted", { opaqueId, deliveryId: result.deliveryId }); + return json({ received: true, deliveryId: result.deliveryFriendlyId }, { status: 200 }); + case "handshake": + // Provider handshake echo (e.g. Slack url_verification): the challenge value, plain text, 200. + return new Response(result.body, { status: 200, headers: { "content-type": "text/plain" } }); + case "duplicate": + return json({ received: true, deliveryId: result.deliveryId }, { status: 200 }); + case "endpoint_not_found": + case "endpoint_inactive": + return json({ error: "Not found" }, { status: 404 }); + case "secret_missing": + logger.warn("webhook ingress rejected: signing secret unset", { opaqueId }); + return json({ error: "Bad request" }, { status: 400 }); + case "verification_failed": + logger.info("webhook ingress verification failed", { opaqueId }); + return json({ error: "Bad request" }, { status: 400 }); + case "enqueue_failed": + logger.error("webhook ingress enqueue failed", { opaqueId, error: result.error }); + return json({ error: "Internal error" }, { status: 500 }); + } +} diff --git a/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts b/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts index c471ce30d8f..f5d44f07604 100644 --- a/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts +++ b/apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts @@ -191,6 +191,33 @@ function initializeSessionsReplicationClickhouseClient(): ClickHouse { }); } +const defaultWebhookDeliveriesReplicationClickhouseClient = singleton( + "webhookDeliveriesReplicationClickhouseClient", + initializeWebhookDeliveriesReplicationClickhouseClient +); + +function initializeWebhookDeliveriesReplicationClickhouseClient(): ClickHouse { + if (!env.WEBHOOK_DELIVERIES_REPLICATION_CLICKHOUSE_URL) { + // Webhook deliveries replication worker gates on this URL; factory may still resolve "webhook_deliveries_replication" for tests. + return defaultClickhouseClient; + } + + const url = new URL(env.WEBHOOK_DELIVERIES_REPLICATION_CLICKHOUSE_URL); + url.searchParams.delete("secure"); + + return new ClickHouse({ + url: url.toString(), + name: "webhook-deliveries-replication", + keepAlive: { + enabled: env.SESSION_REPLICATION_KEEP_ALIVE_ENABLED === "1", + idleSocketTtl: env.SESSION_REPLICATION_KEEP_ALIVE_IDLE_SOCKET_TTL_MS, + }, + logLevel: env.SESSION_REPLICATION_CLICKHOUSE_LOG_LEVEL, + compression: { request: true }, + maxOpenConnections: env.SESSION_REPLICATION_MAX_OPEN_CONNECTIONS, + }); +} + /** Run-engine PendingVersionSystem lookup (`RUN_ENGINE_CLICKHOUSE_URL`); * falls back to the default client if unset. */ const defaultRunEngineClickhouseClient = singleton( @@ -344,6 +371,7 @@ export type ClientType = | "events" | "replication" | "sessions_replication" + | "webhook_deliveries_replication" | "logs" | "query" | "admin" @@ -384,6 +412,9 @@ function buildOrgClickhouseClient(url: string, clientType: ClientType): ClickHou maxOpenConnections: env.RUN_REPLICATION_MAX_OPEN_CONNECTIONS, }); case "sessions_replication": + // Webhook deliveries replication shares the sessions replication ClickHouse + // client config (same infra, both replication writers). + case "webhook_deliveries_replication": return new ClickHouse({ url: parsed.toString(), name, @@ -497,6 +528,8 @@ export class ClickhouseFactory { return defaultRunsReplicationClickhouseClient; case "sessions_replication": return defaultSessionsReplicationClickhouseClient; + case "webhook_deliveries_replication": + return defaultWebhookDeliveriesReplicationClickhouseClient; case "logs": return defaultLogsClickhouseClient; case "query": diff --git a/apps/webapp/app/services/realtime/sessions.server.ts b/apps/webapp/app/services/realtime/sessions.server.ts index 7f50450c3a2..2dd60692b2b 100644 --- a/apps/webapp/app/services/realtime/sessions.server.ts +++ b/apps/webapp/app/services/realtime/sessions.server.ts @@ -1,8 +1,11 @@ -import type { PrismaClient, Session } from "@trigger.dev/database"; -import type { SessionItem } from "@trigger.dev/core/v3"; +import type { Prisma, PrismaClient, Session } from "@trigger.dev/database"; +import type { SessionItem, SessionTriggerConfig } from "@trigger.dev/core/v3"; +import { SessionId } from "@trigger.dev/core/v3/isomorphic"; import type { RunStore } from "@internal/run-store"; import { $replica, prisma } from "~/db.server"; -import { runStore as defaultRunStore } from "~/v3/runStore.server"; +import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; +import { chatSnapshotStoragePathForSession } from "~/services/realtime/chatSnapshot.server"; +import { runStore, runStore as defaultRunStore } from "~/v3/runStore.server"; /** * Prefix that {@link SessionId.generate} attaches to every Session friendlyId. @@ -62,6 +65,71 @@ export function isSessionFriendlyIdForm(value: string): boolean { return value.startsWith(SESSION_FRIENDLY_ID_PREFIX); } +/** + * Find-or-create a Session row. Idempotent on `(environment, externalId)`: two concurrent callers + * converge to the same row (and `triggerConfig` is refreshed on the cached path so a redeployed + * config propagates to the next run). Shared by `POST /api/v1/sessions` and the webhook session + * delivery port so the create-field set (org/env scoping, streamBasin, snapshot path) stays in one place. + */ +export async function findOrCreateSession(params: { + environment: AuthenticatedEnvironment; + externalId?: string; + type: string; + taskIdentifier: string; + triggerConfig: SessionTriggerConfig; + tags?: string[]; + metadata?: Record; + expiresAt?: Date | null; +}): Promise<{ session: Session; isCached: boolean }> { + const { id, friendlyId } = SessionId.generate(); + const env = params.environment; + const triggerConfigJson = params.triggerConfig as unknown as Prisma.InputJsonValue; + + const common = { + type: params.type, + taskIdentifier: params.taskIdentifier, + triggerConfig: triggerConfigJson, + tags: params.tags ?? [], + metadata: params.metadata as Prisma.InputJsonValue | undefined, + expiresAt: params.expiresAt ?? null, + projectId: env.projectId, + runtimeEnvironmentId: env.id, + environmentType: env.type, + organizationId: env.organizationId, + streamBasinName: env.organization.streamBasinName, + chatSnapshotStoragePath: chatSnapshotStoragePathForSession(friendlyId), + }; + + if (params.externalId) { + const session = await prisma.session.upsert({ + where: { + runtimeEnvironmentId_externalId: { + runtimeEnvironmentId: env.id, + externalId: params.externalId, + }, + }, + create: { id, friendlyId, externalId: params.externalId, ...common }, + update: { triggerConfig: triggerConfigJson }, + }); + return { session, isCached: session.id !== id }; + } + + const session = await prisma.session.create({ data: { id, friendlyId, ...common } }); + return { session, isCached: false }; +} + +/** Find a session by externalId without creating one (resume-only channel delivery, e.g. startOn). */ +export async function findSessionByExternalId( + environment: AuthenticatedEnvironment, + externalId: string +): Promise { + return prisma.session.findUnique({ + where: { + runtimeEnvironmentId_externalId: { runtimeEnvironmentId: environment.id, externalId }, + }, + }); +} + /** * Canonicalise the addressing key used for everything stream-level: the * S2 stream path and the run-engine waitpoint cache key. `chat.agent` diff --git a/apps/webapp/app/services/taskIdentifierRegistry.server.ts b/apps/webapp/app/services/taskIdentifierRegistry.server.ts index d7dc93ba31e..c800035b5f7 100644 --- a/apps/webapp/app/services/taskIdentifierRegistry.server.ts +++ b/apps/webapp/app/services/taskIdentifierRegistry.server.ts @@ -15,6 +15,7 @@ import { function toTriggerSource(source: string | undefined): TaskTriggerSource { const normalized = source?.toUpperCase(); if (normalized === "AGENT") return "AGENT"; + if (normalized === "WEBHOOK") return "WEBHOOK"; if (normalized === "SCHEDULED" || normalized === "SCHEDULE") return "SCHEDULED"; return "STANDARD"; } diff --git a/apps/webapp/app/services/webhookConsoleSendRateLimit.server.ts b/apps/webapp/app/services/webhookConsoleSendRateLimit.server.ts new file mode 100644 index 00000000000..7e7ed3584ea --- /dev/null +++ b/apps/webapp/app/services/webhookConsoleSendRateLimit.server.ts @@ -0,0 +1,14 @@ +import { Ratelimit } from "@upstash/ratelimit"; +import { RateLimiter, type Duration } from "./rateLimiter.server"; + +/** + * Per-user limiter for the authenticated webhook console test-send. The console injects deliveries + * in-process (not through the public ingress), so it does NOT ride the per-opaqueId ingress limiter; + * this keeps a single user's testing from flooding the engine + task queue, keyed by user id. + */ +export const webhookConsoleSendRateLimiter = new RateLimiter({ + keyPrefix: "webhook-console-send", + limiter: Ratelimit.fixedWindow(30, "60 s" as Duration), + logSuccess: false, + logFailure: true, +}); diff --git a/apps/webapp/app/services/webhookDeliveriesReplicationInstance.server.ts b/apps/webapp/app/services/webhookDeliveriesReplicationInstance.server.ts new file mode 100644 index 00000000000..191cd6cadbe --- /dev/null +++ b/apps/webapp/app/services/webhookDeliveriesReplicationInstance.server.ts @@ -0,0 +1,92 @@ +import invariant from "tiny-invariant"; +import { env } from "~/env.server"; +import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; +import { singleton } from "~/utils/singleton"; +import { meter, provider } from "~/v3/tracer.server"; +import { WebhookDeliveriesReplicationService } from "./webhookDeliveriesReplicationService.server"; +import { signalsEmitter } from "./signals.server"; + +export const webhookDeliveriesReplicationInstance = singleton( + "webhookDeliveriesReplicationInstance", + initializeWebhookDeliveriesReplicationInstance +); + +function initializeWebhookDeliveriesReplicationInstance() { + const { DATABASE_URL } = process.env; + invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set"); + + if (!env.WEBHOOK_DELIVERIES_REPLICATION_CLICKHOUSE_URL) { + console.log("🗃️ Webhook deliveries replication service not enabled"); + return; + } + + console.log("🗃️ Webhook deliveries replication service enabled"); + + const service = new WebhookDeliveriesReplicationService({ + clickhouseFactory, + // Follows the webhook writer DB (where WebhookDelivery physically lives once split). + pgConnectionUrl: env.WEBHOOK_DATABASE_URL ?? DATABASE_URL, + serviceName: "webhook-deliveries-replication", + slotName: env.WEBHOOK_DELIVERIES_REPLICATION_SLOT_NAME, + publicationName: env.WEBHOOK_DELIVERIES_REPLICATION_PUBLICATION_NAME, + // The source WebhookDelivery is a partitioned parent; without this the slot stays silent. + publishViaPartitionRoot: true, + redisOptions: { + keyPrefix: "webhook-deliveries-replication:", + port: env.RUN_REPLICATION_REDIS_PORT ?? undefined, + host: env.RUN_REPLICATION_REDIS_HOST ?? undefined, + username: env.RUN_REPLICATION_REDIS_USERNAME ?? undefined, + password: env.RUN_REPLICATION_REDIS_PASSWORD ?? undefined, + enableAutoPipelining: true, + ...(env.RUN_REPLICATION_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), + }, + maxFlushConcurrency: env.WEBHOOK_DELIVERIES_REPLICATION_MAX_FLUSH_CONCURRENCY, + flushIntervalMs: env.WEBHOOK_DELIVERIES_REPLICATION_FLUSH_INTERVAL_MS, + flushBatchSize: env.WEBHOOK_DELIVERIES_REPLICATION_FLUSH_BATCH_SIZE, + leaderLockTimeoutMs: env.WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_TIMEOUT_MS, + leaderLockExtendIntervalMs: env.WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_EXTEND_INTERVAL_MS, + leaderLockAcquireAdditionalTimeMs: + env.WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_ADDITIONAL_TIME_MS, + leaderLockRetryIntervalMs: env.WEBHOOK_DELIVERIES_REPLICATION_LEADER_LOCK_RETRY_INTERVAL_MS, + ackIntervalSeconds: env.WEBHOOK_DELIVERIES_REPLICATION_ACK_INTERVAL_SECONDS, + logLevel: env.WEBHOOK_DELIVERIES_REPLICATION_LOG_LEVEL, + waitForAsyncInsert: env.WEBHOOK_DELIVERIES_REPLICATION_WAIT_FOR_ASYNC_INSERT === "1", + tracer: provider.getTracer("webhook-deliveries-replication-service"), + meter, + insertMaxRetries: env.WEBHOOK_DELIVERIES_REPLICATION_INSERT_MAX_RETRIES, + insertBaseDelayMs: env.WEBHOOK_DELIVERIES_REPLICATION_INSERT_BASE_DELAY_MS, + insertMaxDelayMs: env.WEBHOOK_DELIVERIES_REPLICATION_INSERT_MAX_DELAY_MS, + insertStrategy: env.WEBHOOK_DELIVERIES_REPLICATION_INSERT_STRATEGY, + }); + + if (env.WEBHOOK_DELIVERIES_REPLICATION_ENABLED === "1") { + // Gate start() on the org data-stores registry being loaded. Starting earlier would + // race the registry load — sync factory lookups would return `null` and route org-scoped + // deliveries to the default ClickHouse, writing them to the wrong cluster. + clickhouseFactory + .isReady() + .then(() => service.start()) + .then(() => { + console.log("🗃️ Webhook deliveries replication service started"); + }) + .catch((error) => { + console.error("🗃️ Webhook deliveries replication service failed to start", { + error, + }); + }); + + // SIGTERM/SIGINT fire during process teardown; wrap the async shutdown so an + // unhandled rejection doesn't bubble past process exit. + const shutdownWebhookDeliveriesReplication = () => { + service.shutdown().catch((error) => { + console.error("🗃️ Webhook deliveries replication service shutdown error", { + error, + }); + }); + }; + signalsEmitter.on("SIGTERM", shutdownWebhookDeliveriesReplication); + signalsEmitter.on("SIGINT", shutdownWebhookDeliveriesReplication); + } + + return service; +} diff --git a/apps/webapp/app/services/webhookDeliveriesReplicationService.server.ts b/apps/webapp/app/services/webhookDeliveriesReplicationService.server.ts new file mode 100644 index 00000000000..0a62e0e4e9e --- /dev/null +++ b/apps/webapp/app/services/webhookDeliveriesReplicationService.server.ts @@ -0,0 +1,821 @@ +import type { ClickHouse, WebhookDeliveryInsertArray } from "@internal/clickhouse"; +import { getWebhookDeliveryField } from "@internal/clickhouse"; +import { type RedisOptions } from "@internal/redis"; +import { + LogicalReplicationClient, + type MessageDelete, + type MessageInsert, + type MessageUpdate, + type PgoutputMessage, +} from "@internal/replication"; +import { + getMeter, + recordSpanError, + startSpan, + trace, + type Counter, + type Histogram, + type Meter, + type Tracer, +} from "@internal/tracing"; +import { Logger, type LogLevel } from "@trigger.dev/core/logger"; +import { tryCatch } from "@trigger.dev/core/utils"; +import { type WebhookDelivery } from "@trigger.dev/database"; +import EventEmitter from "node:events"; +import type { ClickhouseFactory } from "~/services/clickhouse/clickhouseFactory.server"; +import { ConcurrentFlushScheduler } from "./runsReplicationService.server"; + +interface TransactionEvent { + tag: "insert" | "update" | "delete"; + data: T; + raw: MessageInsert | MessageUpdate | MessageDelete; +} + +interface Transaction { + beginStartTimestamp: number; + commitLsn: string | null; + commitEndLsn: string | null; + xid: number; + events: TransactionEvent[]; + replicationLagMs: number; +} + +export type WebhookDeliveriesReplicationServiceOptions = { + clickhouseFactory: ClickhouseFactory; + pgConnectionUrl: string; + serviceName: string; + slotName: string; + publicationName: string; + publishViaPartitionRoot?: boolean; + redisOptions: RedisOptions; + maxFlushConcurrency?: number; + flushIntervalMs?: number; + flushBatchSize?: number; + leaderLockTimeoutMs?: number; + leaderLockExtendIntervalMs?: number; + leaderLockAcquireAdditionalTimeMs?: number; + leaderLockRetryIntervalMs?: number; + ackIntervalSeconds?: number; + acknowledgeTimeoutMs?: number; + logger?: Logger; + logLevel?: LogLevel; + tracer?: Tracer; + meter?: Meter; + waitForAsyncInsert?: boolean; + insertStrategy?: "insert" | "insert_async"; + // Retry configuration for insert operations + insertMaxRetries?: number; + insertBaseDelayMs?: number; + insertMaxDelayMs?: number; +}; + +type WebhookDeliveryInsert = { + _version: bigint; + delivery: WebhookDelivery; + event: "insert" | "update" | "delete"; +}; + +export type WebhookDeliveriesReplicationServiceEvents = { + message: [ + { lsn: string; message: PgoutputMessage; service: WebhookDeliveriesReplicationService }, + ]; + batchFlushed: [{ flushId: string; deliveryInserts: WebhookDeliveryInsertArray[] }]; +}; + +export class WebhookDeliveriesReplicationService { + private _isSubscribed = false; + private _currentTransaction: + | (Omit, "commitEndLsn" | "replicationLagMs"> & { + commitEndLsn?: string | null; + replicationLagMs?: number; + }) + | null = null; + + private _replicationClient: LogicalReplicationClient; + private _concurrentFlushScheduler: ConcurrentFlushScheduler; + private logger: Logger; + private _isShuttingDown = false; + private _isShutDownComplete = false; + private _tracer: Tracer; + private _meter: Meter; + private _currentParseDurationMs: number | null = null; + private _lastAcknowledgedAt: number | null = null; + private _acknowledgeTimeoutMs: number; + private _latestCommitEndLsn: string | null = null; + private _lastAcknowledgedLsn: string | null = null; + private _acknowledgeInterval: NodeJS.Timeout | null = null; + // Retry configuration + private _insertMaxRetries: number; + private _insertBaseDelayMs: number; + private _insertMaxDelayMs: number; + private _insertStrategy: "insert" | "insert_async"; + + // Metrics + private _replicationLagHistogram: Histogram; + private _batchesFlushedCounter: Counter; + private _batchSizeHistogram: Histogram; + private _deliveriesInsertedCounter: Counter; + private _insertRetriesCounter: Counter; + private _eventsProcessedCounter: Counter; + private _flushDurationHistogram: Histogram; + + public readonly events: EventEmitter; + + constructor(private readonly options: WebhookDeliveriesReplicationServiceOptions) { + this.logger = + options.logger ?? + new Logger("WebhookDeliveriesReplicationService", options.logLevel ?? "info"); + this.events = new EventEmitter(); + this._tracer = options.tracer ?? trace.getTracer("webhook-deliveries-replication-service"); + this._meter = options.meter ?? getMeter("webhook-deliveries-replication"); + + // Initialize metrics + this._replicationLagHistogram = this._meter.createHistogram( + "webhook_deliveries_replication.replication_lag_ms", + { + description: "Replication lag from Postgres commit to processing", + unit: "ms", + } + ); + + this._batchesFlushedCounter = this._meter.createCounter( + "webhook_deliveries_replication.batches_flushed", + { + description: "Total batches flushed to ClickHouse", + } + ); + + this._batchSizeHistogram = this._meter.createHistogram( + "webhook_deliveries_replication.batch_size", + { + description: "Number of items per batch flush", + unit: "items", + } + ); + + this._deliveriesInsertedCounter = this._meter.createCounter( + "webhook_deliveries_replication.deliveries_inserted", + { + description: "Webhook delivery inserts to ClickHouse", + unit: "inserts", + } + ); + + this._insertRetriesCounter = this._meter.createCounter( + "webhook_deliveries_replication.insert_retries", + { + description: "Insert retry attempts", + } + ); + + this._eventsProcessedCounter = this._meter.createCounter( + "webhook_deliveries_replication.events_processed", + { + description: "Replication events processed (inserts, updates, deletes)", + } + ); + + this._flushDurationHistogram = this._meter.createHistogram( + "webhook_deliveries_replication.flush_duration_ms", + { + description: "Duration of batch flush operations", + unit: "ms", + } + ); + + this._acknowledgeTimeoutMs = options.acknowledgeTimeoutMs ?? 1_000; + + this._insertStrategy = options.insertStrategy ?? "insert"; + + this._replicationClient = new LogicalReplicationClient({ + pgConfig: { + connectionString: options.pgConnectionUrl, + }, + name: options.serviceName, + slotName: options.slotName, + publicationName: options.publicationName, + table: "WebhookDelivery", + publishViaPartitionRoot: options.publishViaPartitionRoot, + redisOptions: options.redisOptions, + autoAcknowledge: false, + publicationActions: ["insert", "update", "delete"], + logger: options.logger ?? new Logger("LogicalReplicationClient", options.logLevel ?? "info"), + leaderLockTimeoutMs: options.leaderLockTimeoutMs ?? 30_000, + leaderLockExtendIntervalMs: options.leaderLockExtendIntervalMs ?? 10_000, + ackIntervalSeconds: options.ackIntervalSeconds ?? 10, + leaderLockAcquireAdditionalTimeMs: options.leaderLockAcquireAdditionalTimeMs ?? 10_000, + leaderLockRetryIntervalMs: options.leaderLockRetryIntervalMs ?? 500, + tracer: options.tracer, + }); + + this._concurrentFlushScheduler = new ConcurrentFlushScheduler({ + batchSize: options.flushBatchSize ?? 50, + flushInterval: options.flushIntervalMs ?? 100, + maxConcurrency: options.maxFlushConcurrency ?? 100, + callback: this.#flushBatch.bind(this), + // Key-based deduplication to reduce duplicates sent to ClickHouse + getKey: (item) => { + if (!item?.delivery?.id) { + this.logger.warn("Skipping replication event with null delivery", { event: item }); + return null; + } + return `${item.event}_${item.delivery.id}`; + }, + // Keep the delivery with the higher version (latest) + // and take the last occurrence for that version. + // Items originating from the same DB transaction have the same version. + shouldReplace: (existing, incoming) => incoming._version >= existing._version, + logger: new Logger("ConcurrentFlushScheduler", options.logLevel ?? "info"), + tracer: options.tracer, + }); + + this._replicationClient.events.on("data", async ({ lsn, log, parseDuration }) => { + this.#handleData(lsn, log, parseDuration); + }); + + this._replicationClient.events.on("heartbeat", async ({ lsn, shouldRespond }) => { + if (this._isShuttingDown) return; + if (this._isShutDownComplete) return; + + if (shouldRespond) { + this._lastAcknowledgedLsn = lsn; + await this._replicationClient.acknowledge(lsn); + } + }); + + this._replicationClient.events.on("error", (error) => { + this.logger.error("Replication client error", { + error, + }); + }); + + this._replicationClient.events.on("start", () => { + this.logger.info("Replication client started"); + }); + + this._replicationClient.events.on("acknowledge", ({ lsn }) => { + this.logger.debug("Acknowledged", { lsn }); + }); + + this._replicationClient.events.on("leaderElection", (isLeader) => { + this.logger.info("Leader election", { isLeader }); + }); + + // Initialize retry configuration + this._insertMaxRetries = options.insertMaxRetries ?? 3; + this._insertBaseDelayMs = options.insertBaseDelayMs ?? 100; + this._insertMaxDelayMs = options.insertMaxDelayMs ?? 2000; + } + + public async shutdown() { + if (this._isShuttingDown) return; + + this._isShuttingDown = true; + + this.logger.info("Initiating shutdown of webhook deliveries replication service"); + + if (!this._currentTransaction) { + this.logger.info("No transaction to commit, shutting down immediately"); + await this._replicationClient.stop(); + this._isSubscribed = false; + this._isShutDownComplete = true; + return; + } + + this._concurrentFlushScheduler.shutdown(); + } + + async start() { + if (this._isSubscribed) { + this.logger.debug("Replication client already started, skipping start"); + return; + } + + this.logger.info("Starting replication client", { + lastLsn: this._latestCommitEndLsn, + }); + + await this._replicationClient.subscribe(this._latestCommitEndLsn ?? undefined); + + this._acknowledgeInterval = setInterval(this.#acknowledgeLatestTransaction.bind(this), 1000); + this._concurrentFlushScheduler.start(); + this._isSubscribed = true; + } + + async stop() { + this.logger.info("Stopping replication client"); + + await this._replicationClient.stop(); + + if (this._acknowledgeInterval) { + clearInterval(this._acknowledgeInterval); + this._acknowledgeInterval = null; + } + + this._isSubscribed = false; + } + + async teardown() { + this.logger.info("Teardown replication client"); + + await this._replicationClient.teardown(); + + if (this._acknowledgeInterval) { + clearInterval(this._acknowledgeInterval); + this._acknowledgeInterval = null; + } + + this._isSubscribed = false; + } + + #handleData(lsn: string, message: PgoutputMessage, parseDuration: bigint) { + this.logger.debug("Handling data", { + lsn, + tag: message.tag, + parseDuration, + }); + + this.events.emit("message", { lsn, message, service: this }); + + switch (message.tag) { + case "begin": { + if (this._isShuttingDown || this._isShutDownComplete) { + return; + } + + this._currentTransaction = { + beginStartTimestamp: Date.now(), + commitLsn: message.commitLsn, + xid: message.xid, + events: [], + }; + + this._currentParseDurationMs = Number(parseDuration) / 1_000_000; + + break; + } + case "insert": { + if (!this._currentTransaction) { + return; + } + + if (this._currentParseDurationMs) { + this._currentParseDurationMs = + this._currentParseDurationMs + Number(parseDuration) / 1_000_000; + } + + this._currentTransaction.events.push({ + tag: message.tag, + data: message.new as WebhookDelivery, + raw: message, + }); + break; + } + case "update": { + if (!this._currentTransaction) { + return; + } + + if (this._currentParseDurationMs) { + this._currentParseDurationMs = + this._currentParseDurationMs + Number(parseDuration) / 1_000_000; + } + + this._currentTransaction.events.push({ + tag: message.tag, + data: message.new as WebhookDelivery, + raw: message, + }); + break; + } + case "delete": { + if (!this._currentTransaction) { + return; + } + + if (this._currentParseDurationMs) { + this._currentParseDurationMs = + this._currentParseDurationMs + Number(parseDuration) / 1_000_000; + } + + this._currentTransaction.events.push({ + tag: message.tag, + data: message.old as WebhookDelivery, + raw: message, + }); + + break; + } + case "commit": { + if (!this._currentTransaction) { + return; + } + + if (this._currentParseDurationMs) { + this._currentParseDurationMs = + this._currentParseDurationMs + Number(parseDuration) / 1_000_000; + } + + const replicationLagMs = Date.now() - Number(message.commitTime / 1000n); + this._currentTransaction.commitEndLsn = message.commitEndLsn; + this._currentTransaction.replicationLagMs = replicationLagMs; + const transaction = this._currentTransaction as Transaction; + this._currentTransaction = null; + + if (transaction.commitEndLsn) { + this._latestCommitEndLsn = transaction.commitEndLsn; + } + + this.#handleTransaction(transaction); + break; + } + default: { + this.logger.debug("Unknown message tag", { + pgMessage: message, + }); + } + } + } + + #handleTransaction(transaction: Transaction) { + if (this._isShutDownComplete) return; + + if (this._isShuttingDown) { + this._replicationClient.stop().finally(() => { + this._isSubscribed = false; + this._isShutDownComplete = true; + }); + } + + // If there are no events, do nothing + if (transaction.events.length === 0) { + return; + } + + if (!transaction.commitEndLsn) { + this.logger.error("Transaction has no commit end lsn", { + transaction, + }); + + return; + } + + const lsnToUInt64Start = process.hrtime.bigint(); + + // If there are events, we need to handle them + const _version = lsnToUInt64(transaction.commitEndLsn); + + const lsnToUInt64DurationMs = Number(process.hrtime.bigint() - lsnToUInt64Start) / 1_000_000; + + this._concurrentFlushScheduler.addToBatch( + transaction.events.map((event) => ({ + _version, + delivery: event.data, + event: event.tag, + })) + ); + + // Record metrics + this._replicationLagHistogram.record(transaction.replicationLagMs); + + // Count events by type + for (const event of transaction.events) { + this._eventsProcessedCounter.add(1, { event_type: event.tag }); + } + + this.logger.debug("handle_transaction", { + transaction: { + xid: transaction.xid, + commitLsn: transaction.commitLsn, + commitEndLsn: transaction.commitEndLsn, + events: transaction.events.length, + parseDurationMs: this._currentParseDurationMs, + lsnToUInt64DurationMs, + version: _version.toString(), + }, + }); + } + + async #acknowledgeLatestTransaction() { + if (!this._latestCommitEndLsn) { + return; + } + + if (this._lastAcknowledgedLsn === this._latestCommitEndLsn) { + return; + } + + const now = Date.now(); + + if (this._lastAcknowledgedAt) { + const timeSinceLastAcknowledged = now - this._lastAcknowledgedAt; + // If we've already acknowledged within the last second, don't acknowledge again + if (timeSinceLastAcknowledged < this._acknowledgeTimeoutMs) { + return; + } + } + + this._lastAcknowledgedAt = now; + this._lastAcknowledgedLsn = this._latestCommitEndLsn; + + this.logger.debug("acknowledge_latest_transaction", { + commitEndLsn: this._latestCommitEndLsn, + lastAcknowledgedAt: this._lastAcknowledgedAt, + }); + + const [ackError] = await tryCatch( + this._replicationClient.acknowledge(this._latestCommitEndLsn) + ); + + if (ackError) { + this.logger.error("Error acknowledging transaction", { ackError }); + } + + if (this._isShutDownComplete && this._acknowledgeInterval) { + clearInterval(this._acknowledgeInterval); + } + } + + async #flushBatch(flushId: string, batch: Array) { + if (batch.length === 0) { + return; + } + + this.logger.debug("Flushing batch", { + flushId, + batchSize: batch.length, + }); + + const flushStartTime = performance.now(); + + await startSpan(this._tracer, "flushBatch", async (span) => { + const routeCache = new Map(); + const groups = new Map(); + + for (const item of batch) { + if (!item.delivery.organizationId) { + continue; + } + + let client = routeCache.get(item.delivery.organizationId); + if (!client) { + client = this.options.clickhouseFactory.getClickhouseForOrganizationSync( + item.delivery.organizationId, + "webhook_deliveries_replication" + ); + routeCache.set(item.delivery.organizationId, client); + } + + let group = groups.get(client); + if (!group) { + group = { deliveryInserts: [] }; + groups.set(client, group); + } + + group.deliveryInserts.push( + toWebhookDeliveryInsertArray(item.delivery, item._version, item.event === "delete") + ); + } + + // batch inserts in clickhouse are more performant if the items + // are pre-sorted by the primary key + const sortDeliveryInserts = (rows: WebhookDeliveryInsertArray[]) => + rows.sort((a, b) => { + const aOrgId = getWebhookDeliveryField(a, "organization_id"); + const bOrgId = getWebhookDeliveryField(b, "organization_id"); + if (aOrgId !== bOrgId) { + return aOrgId < bOrgId ? -1 : 1; + } + const aProjId = getWebhookDeliveryField(a, "project_id"); + const bProjId = getWebhookDeliveryField(b, "project_id"); + if (aProjId !== bProjId) { + return aProjId < bProjId ? -1 : 1; + } + const aEnvId = getWebhookDeliveryField(a, "environment_id"); + const bEnvId = getWebhookDeliveryField(b, "environment_id"); + if (aEnvId !== bEnvId) { + return aEnvId < bEnvId ? -1 : 1; + } + const aCreatedAt = getWebhookDeliveryField(a, "created_at"); + const bCreatedAt = getWebhookDeliveryField(b, "created_at"); + if (aCreatedAt !== bCreatedAt) { + return aCreatedAt - bCreatedAt; + } + const aDeliveryId = getWebhookDeliveryField(a, "delivery_id"); + const bDeliveryId = getWebhookDeliveryField(b, "delivery_id"); + if (aDeliveryId === bDeliveryId) return 0; + return aDeliveryId < bDeliveryId ? -1 : 1; + }); + + const combinedDeliveryInserts: WebhookDeliveryInsertArray[] = []; + let deliveryError: Error | null = null; + + // Sequential per-group flush — matches runsReplicationService for the same reason + // (parallel writes have hit Linux net.ipv4.tcp_wmem buffer pressure at high throughput). + for (const [clickhouse, group] of groups) { + sortDeliveryInserts(group.deliveryInserts); + combinedDeliveryInserts.push(...group.deliveryInserts); + + const [insErr] = await this.#insertWithRetry( + (attempt) => this.#insertDeliveryInserts(clickhouse, group.deliveryInserts, attempt), + "delivery inserts", + flushId + ); + if (insErr && !deliveryError) { + deliveryError = insErr; + } + + if (!insErr) { + this._deliveriesInsertedCounter.add(group.deliveryInserts.length); + } + } + + span.setAttribute("delivery_inserts", combinedDeliveryInserts.length); + + this.logger.debug("Flushing inserts", { + flushId, + deliveryInserts: combinedDeliveryInserts.length, + clickhouseGroups: groups.size, + }); + + if (deliveryError) { + this.logger.error("Error inserting delivery inserts", { + error: deliveryError, + flushId, + }); + recordSpanError(span, deliveryError); + } + + this.logger.debug("Flushed inserts", { + flushId, + deliveryInserts: combinedDeliveryInserts.length, + }); + + this.events.emit("batchFlushed", { flushId, deliveryInserts: combinedDeliveryInserts }); + + const flushDurationMs = performance.now() - flushStartTime; + const hasErrors = deliveryError !== null; + + this._batchSizeHistogram.record(batch.length); + this._flushDurationHistogram.record(flushDurationMs); + this._batchesFlushedCounter.add(1, { success: !hasErrors }); + }); + } + + // New method to handle inserts with retry logic for connection errors + async #insertWithRetry( + insertFn: (attempt: number) => Promise, + operationName: string, + flushId: string + ): Promise<[Error | null, T | null]> { + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= this._insertMaxRetries; attempt++) { + try { + const result = await insertFn(attempt); + return [null, result]; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + // Check if this is a retryable error + if (this.#isRetryableError(lastError)) { + const delay = this.#calculateRetryDelay(attempt); + + this.logger.warn(`Retrying WebhookDeliveriesReplication insert due to error`, { + operationName, + flushId, + attempt, + maxRetries: this._insertMaxRetries, + error: lastError.message, + delay, + }); + + // Record retry metric + this._insertRetriesCounter.add(1, { operation: "webhook_deliveries" }); + + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + break; + } + } + + return [lastError, null]; + } + + // Retry all errors except known permanent ones + #isRetryableError(error: Error): boolean { + const errorMessage = error.message.toLowerCase(); + + // Permanent errors that should NOT be retried + const permanentErrorPatterns = [ + "authentication failed", + "permission denied", + "invalid credentials", + "table not found", + "database not found", + "column not found", + "schema mismatch", + "invalid query", + "syntax error", + "type error", + "constraint violation", + "duplicate key", + "foreign key violation", + ]; + + // If it's a known permanent error, don't retry + if (permanentErrorPatterns.some((pattern) => errorMessage.includes(pattern))) { + return false; + } + + // Retry everything else + return true; + } + + #calculateRetryDelay(attempt: number): number { + // Exponential backoff: baseDelay, baseDelay*2, baseDelay*4, etc. + const delay = Math.min( + this._insertBaseDelayMs * Math.pow(2, attempt - 1), + this._insertMaxDelayMs + ); + + // Add some jitter to prevent thundering herd + const jitter = Math.random() * 100; + return delay + jitter; + } + + #getClickhouseInsertSettings() { + if (this._insertStrategy === "insert") { + return {}; + } + + return { + async_insert: 1 as const, + async_insert_max_data_size: "1000000", + async_insert_busy_timeout_ms: 1000, + wait_for_async_insert: this.options.waitForAsyncInsert ? (1 as const) : (0 as const), + }; + } + + async #insertDeliveryInserts( + clickhouse: ClickHouse, + deliveryInserts: WebhookDeliveryInsertArray[], + attempt: number + ) { + if (deliveryInserts.length === 0) { + return; + } + return await startSpan(this._tracer, "insertDeliveryInserts", async (span) => { + const [insertError, insertResult] = await clickhouse.webhookDeliveries.insertCompactArrays( + deliveryInserts, + { + params: { + clickhouse_settings: this.#getClickhouseInsertSettings(), + }, + } + ); + + if (insertError) { + this.logger.error("Error inserting delivery inserts attempt", { + error: insertError, + attempt, + }); + + recordSpanError(span, insertError); + throw insertError; + } + + return insertResult; + }); + } +} + +function toWebhookDeliveryInsertArray( + delivery: WebhookDelivery, + version: bigint, + isDeleted: boolean +): WebhookDeliveryInsertArray { + return [ + delivery.runtimeEnvironmentId, + delivery.organizationId, + delivery.projectId, + delivery.id, + delivery.webhookEndpointId, + delivery.environmentType, + delivery.friendlyId, + delivery.externalDeliveryId ?? "", + delivery.runId ?? "", + delivery.status, + delivery.isTest ? 1 : 0, + delivery.createdAt.getTime(), + delivery.updatedAt.getTime(), + version.toString(), + isDeleted ? 1 : 0, + ]; +} + +function lsnToUInt64(lsn: string): bigint { + const [seg, off] = lsn.split("/"); + return (BigInt("0x" + seg) << 32n) | BigInt("0x" + off); +} diff --git a/apps/webapp/app/services/webhookDeliveriesRepository/clickhouseWebhookDeliveriesRepository.server.ts b/apps/webapp/app/services/webhookDeliveriesRepository/clickhouseWebhookDeliveriesRepository.server.ts new file mode 100644 index 00000000000..799e06b0c90 --- /dev/null +++ b/apps/webapp/app/services/webhookDeliveriesRepository/clickhouseWebhookDeliveriesRepository.server.ts @@ -0,0 +1,372 @@ +import { type ClickhouseQueryBuilder } from "@internal/clickhouse"; +import { decodeRunsCursor, encodeRunsCursor } from "../runsRepository/runsCursor.server"; +import { + type CountDeliveriesByEndpointOptions, + type DetailedWebhookDelivery, + type FilterWebhookDeliveriesOptions, + type GetDeliveriesByFriendlyIdsOptions, + type GetWebhookDeliveryOptions, + type IWebhookDeliveriesRepository, + type ListedWebhookDelivery, + type ListWebhookDeliveriesOptions, + type WebhookDeliveriesRepositoryOptions, + type WebhookDeliveryIdsPage, +} from "./webhookDeliveriesRepository.server"; + +type DeliveryCursorRow = { deliveryId: string; createdAt: number }; + +const WEBHOOK_DELIVERY_ID_PREFIX = "whd_"; + +const DELIVERY_DETAIL_SELECT = { + id: true, + friendlyId: true, + webhookEndpointId: true, + runtimeEnvironmentId: true, + environmentType: true, + status: true, + externalDeliveryId: true, + idempotencyKey: true, + runId: true, + rawBodyHash: true, + parsedEvent: true, + headers: true, + errorMessage: true, + filterReason: true, + createdAt: true, + updatedAt: true, + processedAt: true, +} as const; + +const DELIVERY_LIST_SELECT = { + id: true, + friendlyId: true, + webhookEndpointId: true, + runtimeEnvironmentId: true, + status: true, + isTest: true, + externalDeliveryId: true, + runId: true, + createdAt: true, + processedAt: true, + errorMessage: true, +} as const; + +export class ClickHouseWebhookDeliveriesRepository implements IWebhookDeliveriesRepository { + constructor(private readonly options: WebhookDeliveriesRepositoryOptions) {} + + get name() { + return "clickhouse"; + } + + /** + * Runs the keyset-paginated query and returns `{ deliveryId, createdAt }` rows + * (one extra beyond `page.size` to signal "has more"). The ordering is always + * the composite `(created_at, delivery_id)`; the cursor predicate must match + * it. The cursor is the shared opaque `runsCursor` token — its `runId` field + * carries the delivery id here. + */ + private async listDeliveryRows( + options: ListWebhookDeliveriesOptions + ): Promise { + const queryBuilder = this.options.clickhouse.webhookDeliveries.queryBuilder(); + applyDeliveryFiltersToQueryBuilder(queryBuilder, options); + + const forward = options.page.direction === "forward" || !options.page.direction; + + if (options.page.cursor) { + const decoded = decodeRunsCursor(options.page.cursor); + + if (forward) { + if (decoded.kind === "composite") { + queryBuilder.where( + "(created_at, delivery_id) < (fromUnixTimestamp64Milli({cursorCreatedAt: Int64}), {deliveryId: String})", + { cursorCreatedAt: decoded.createdAt, deliveryId: decoded.runId } + ); + } else { + queryBuilder.where("delivery_id < {deliveryId: String}", { deliveryId: decoded.runId }); + } + queryBuilder.orderBy("created_at DESC, delivery_id DESC"); + } else { + if (decoded.kind === "composite") { + queryBuilder.where( + "(created_at, delivery_id) > (fromUnixTimestamp64Milli({cursorCreatedAt: Int64}), {deliveryId: String})", + { cursorCreatedAt: decoded.createdAt, deliveryId: decoded.runId } + ); + } else { + queryBuilder.where("delivery_id > {deliveryId: String}", { deliveryId: decoded.runId }); + } + queryBuilder.orderBy("created_at ASC, delivery_id ASC"); + } + + queryBuilder.limit(options.page.size + 1); + } else { + // Initial page - no cursor provided + queryBuilder.orderBy("created_at DESC, delivery_id DESC").limit(options.page.size + 1); + } + + const [queryError, result] = await queryBuilder.execute(); + + if (queryError) { + throw queryError; + } + + return result.map((row) => ({ deliveryId: row.delivery_id, createdAt: row.created_at_ms })); + } + + /** + * Turns the (size + 1) result rows into the actual page of rows plus the + * forward/backward cursors. Mirrors ClickHouseRunsRepository.listRunIds; the + * cursor tokens are composite so pagination can't duplicate or skip + * deliveries. Shared by listDeliveryIds and listDeliveries so the hydration + * path can reuse the page rows (and their created_at values) directly. + */ + private buildPage( + rows: DeliveryCursorRow[], + options: ListWebhookDeliveriesOptions + ): { + pageRows: DeliveryCursorRow[]; + pagination: { nextCursor: string | null; previousCursor: string | null }; + } { + // listDeliveryRows fetches one extra row beyond page.size to detect "has more". + const hasMore = rows.length > options.page.size; + + const cursorFor = (row: DeliveryCursorRow | undefined): string | null => + row ? encodeRunsCursor(row.createdAt, row.deliveryId) : null; + + let nextCursor: string | null = null; + let previousCursor: string | null = null; + + const direction = options.page.direction ?? "forward"; + switch (direction) { + case "forward": { + previousCursor = options.page.cursor ? cursorFor(rows.at(0)) : null; + if (hasMore) { + // The next cursor is the last delivery on this page. + nextCursor = cursorFor(rows[options.page.size - 1]); + } + break; + } + case "backward": { + const reversedRows = [...rows].reverse(); + if (hasMore) { + previousCursor = cursorFor(reversedRows.at(1)); + nextCursor = cursorFor(reversedRows.at(options.page.size)); + } else { + // No newer rows, so there's no previous (newer) page. The next + // (older) cursor is the oldest row on this page = rows[0] (rows are + // ASC here). Index by the actual row count, not page.size — on a + // partial page (fewer than page.size rows) page.size-1 overshoots + // and would null the cursor, stranding forward navigation. + nextCursor = cursorFor(rows.at(0)); + } + break; + } + } + + // The page is always the first `page.size` rows of the result. listDeliveryRows + // fetches one extra row only to detect `hasMore`; that extra row is the + // farthest from the cursor in BOTH directions (forward orders DESC, backward + // orders ASC), so it's always the trailing element to drop — never the + // leading one. + const pageRows = rows.slice(0, options.page.size); + + return { pageRows, pagination: { nextCursor, previousCursor } }; + } + + async listDeliveryIds(options: ListWebhookDeliveriesOptions): Promise { + const rows = await this.listDeliveryRows(options); + const { pageRows, pagination } = this.buildPage(rows, options); + + return { deliveryIds: pageRows.map((row) => row.deliveryId), pagination }; + } + + async listDeliveries(options: ListWebhookDeliveriesOptions) { + const rows = await this.listDeliveryRows(options); + const { pageRows, pagination } = this.buildPage(rows, options); + + if (pageRows.length === 0) { + return { deliveries: [], pagination }; + } + + const deliveryIds = pageRows.map((row) => row.deliveryId); + + // PARTITION PRUNING: webhookDelivery is RANGE-partitioned on createdAt. An + // `id IN (...)` query without a createdAt predicate scans every child + // partition, so derive a [min, max] range from the CH page and pass it + // through. This is the one place webhook hydration diverges from runs. + const createdAtMsValues = pageRows.map((row) => row.createdAt); + const minCreatedAtMs = Math.min(...createdAtMsValues); + const maxCreatedAtMs = Math.max(...createdAtMsValues); + + // CH gives the ordered id list; Postgres hydrates the full lean rows by PK id. + const deliveries = await this.options.prisma.webhookDelivery.findMany({ + where: { + id: { in: deliveryIds }, + createdAt: { + gte: new Date(minCreatedAtMs), + lte: new Date(maxCreatedAtMs), + }, + }, + select: DELIVERY_LIST_SELECT, + }); + + // Re-order to CH order (findMany does not preserve `in` order). + const byId = new Map(deliveries.map((d) => [d.id, d])); + let result = deliveryIds + .map((id) => byId.get(id)) + .filter((d): d is (typeof deliveries)[number] => Boolean(d)); + + // ClickHouse is slightly delayed, so re-filter status in memory too. + if (options.statuses && options.statuses.length > 0) { + result = result.filter((d) => options.statuses!.includes(d.status)); + } + + return { deliveries: result, pagination }; + } + + async countDeliveries(options: FilterWebhookDeliveriesOptions) { + const queryBuilder = this.options.clickhouse.webhookDeliveries.countQueryBuilder(); + applyDeliveryFiltersToQueryBuilder(queryBuilder, options); + + const [queryError, result] = await queryBuilder.execute(); + + if (queryError) { + throw queryError; + } + + if (result.length === 0) { + throw new Error("No count rows returned"); + } + + return result[0].count; + } + + async countDeliveriesByEndpoint( + options: CountDeliveriesByEndpointOptions + ): Promise> { + if (options.webhookEndpointIds.length === 0) { + return new Map(); + } + + const queryBuilder = this.options.clickhouse.webhookDeliveries.groupedCountQueryBuilder(); + queryBuilder + .where("organization_id = {organizationId: String}", { + organizationId: options.organizationId, + }) + .where("project_id = {projectId: String}", { projectId: options.projectId }) + .where("environment_id = {environmentId: String}", { environmentId: options.environmentId }) + .where("webhook_endpoint_id IN {webhookEndpointIds: Array(String)}", { + webhookEndpointIds: options.webhookEndpointIds, + }) + .where("created_at >= fromUnixTimestamp64Milli({period: Int64})", { + period: Date.now() - options.period, + }) + .groupBy("webhook_endpoint_id"); + + const [queryError, rows] = await queryBuilder.execute(); + + if (queryError) { + throw queryError; + } + + const counts = new Map(); + for (const row of rows) { + counts.set(row.webhook_endpoint_id, row.count); + } + return counts; + } + + /** + * A point lookup: pure Postgres, never ClickHouse. `friendlyId` is `whd_` + the row id, so we + * strip the prefix and hit the composite-PK index (scoped by environment). ClickHouse is for + * aggregations and for filtering/ordering a list into ids, never for selecting a row's columns. + */ + async getDelivery(options: GetWebhookDeliveryOptions): Promise { + const id = options.friendlyId.startsWith(WEBHOOK_DELIVERY_ID_PREFIX) + ? options.friendlyId.slice(WEBHOOK_DELIVERY_ID_PREFIX.length) + : options.friendlyId; + + return this.options.prisma.webhookDelivery.findFirst({ + where: { id, runtimeEnvironmentId: options.environmentId }, + select: DELIVERY_DETAIL_SELECT, + }); + } + + /** + * Hydrate a known set of deliveries by friendlyId. Pure Postgres: the caller (the live poll) + * already has the ids, so there is nothing for ClickHouse to filter or order. + */ + async getDeliveriesByFriendlyIds( + options: GetDeliveriesByFriendlyIdsOptions + ): Promise { + const ids = options.friendlyIds.map((friendlyId) => + friendlyId.startsWith(WEBHOOK_DELIVERY_ID_PREFIX) + ? friendlyId.slice(WEBHOOK_DELIVERY_ID_PREFIX.length) + : friendlyId + ); + if (ids.length === 0) return []; + + return this.options.prisma.webhookDelivery.findMany({ + where: { id: { in: ids }, runtimeEnvironmentId: options.environmentId }, + select: DELIVERY_LIST_SELECT, + }); + } +} + +function applyDeliveryFiltersToQueryBuilder( + queryBuilder: ClickhouseQueryBuilder, + options: FilterWebhookDeliveriesOptions +) { + queryBuilder + .where("organization_id = {organizationId: String}", { + organizationId: options.organizationId, + }) + .where("project_id = {projectId: String}", { projectId: options.projectId }) + .where("environment_id = {environmentId: String}", { environmentId: options.environmentId }); + + if (options.webhookEndpointId) { + queryBuilder.where("webhook_endpoint_id = {webhookEndpointId: String}", { + webhookEndpointId: options.webhookEndpointId, + }); + } + + if (options.webhookEndpointIds && options.webhookEndpointIds.length > 0) { + queryBuilder.where("webhook_endpoint_id IN {webhookEndpointIds: Array(String)}", { + webhookEndpointIds: options.webhookEndpointIds, + }); + } + + if (options.deliveryId) { + queryBuilder.where( + "(friendly_id = {deliveryId: String} OR external_delivery_id = {deliveryId: String})", + { deliveryId: options.deliveryId } + ); + } + + if (options.runId) { + queryBuilder.where("run_id = {runId: String}", { runId: options.runId }); + } + + if (options.statuses && options.statuses.length > 0) { + queryBuilder.where("status IN {statuses: Array(String)}", { statuses: options.statuses }); + } + + if (options.isTest !== undefined) { + queryBuilder.where("is_test = {isTest: UInt8}", { isTest: options.isTest ? 1 : 0 }); + } + + // PARTITION PRUNING: the list MUST carry a created_at range. + if (options.period) { + queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", { + period: new Date(Date.now() - options.period).getTime(), + }); + } + if (options.from) { + queryBuilder.where("created_at >= fromUnixTimestamp64Milli({from: Int64})", { + from: options.from, + }); + } + if (options.to) { + queryBuilder.where("created_at <= fromUnixTimestamp64Milli({to: Int64})", { to: options.to }); + } +} diff --git a/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts b/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts new file mode 100644 index 00000000000..8337ee3c080 --- /dev/null +++ b/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts @@ -0,0 +1,231 @@ +import { type ClickHouse } from "@internal/clickhouse"; +import { type Tracer } from "@internal/tracing"; +import { type Logger, type LogLevel } from "@trigger.dev/core/logger"; +import { type Prisma, WebhookDeliveryStatus } from "@trigger.dev/database"; +import { z } from "zod"; +import { type WebhookReplicaDatabase } from "~/db.server"; +import { startActiveSpan } from "~/v3/tracer.server"; +import { ClickHouseWebhookDeliveriesRepository } from "./clickhouseWebhookDeliveriesRepository.server"; + +export type WebhookDeliveriesRepositoryOptions = { + clickhouse: ClickHouse; + prisma: WebhookReplicaDatabase; + logger?: Logger; + logLevel?: LogLevel; + tracer?: Tracer; +}; + +const DeliveryStatus = z.enum( + Object.values(WebhookDeliveryStatus) as [WebhookDeliveryStatus, ...WebhookDeliveryStatus[]] +); + +export type FilterWebhookDeliveriesOptions = { + organizationId: string; + projectId: string; + environmentId: string; + webhookEndpointId?: string; + webhookEndpointIds?: string[]; // webhook (handler) filter, resolved to endpoint ids by the presenter + deliveryId?: string; // matches the delivery friendlyId OR external delivery id + runId?: string; // INTERNAL run id (presenter resolves friendly -> internal) + statuses?: WebhookDeliveryStatus[]; + isTest?: boolean; + period?: number; + from?: number; + to?: number; +}; + +export type ListWebhookDeliveriesOptions = FilterWebhookDeliveriesOptions & { + page: { size: number; cursor?: string; direction?: "forward" | "backward" }; +}; + +export type WebhookDeliveryIdsPage = { + deliveryIds: string[]; + pagination: { nextCursor: string | null; previousCursor: string | null }; +}; + +export type ListedWebhookDelivery = Prisma.WebhookDeliveryGetPayload<{ + select: { + id: true; + friendlyId: true; + webhookEndpointId: true; + runtimeEnvironmentId: true; + status: true; + isTest: true; + externalDeliveryId: true; + runId: true; + createdAt: true; + processedAt: true; + errorMessage: true; + }; +}>; + +// The detail view selects more than the list, including the size-capped +// `parsedEvent` snapshot (which the list omits). +export type DetailedWebhookDelivery = Prisma.WebhookDeliveryGetPayload<{ + select: { + id: true; + friendlyId: true; + webhookEndpointId: true; + runtimeEnvironmentId: true; + environmentType: true; + status: true; + externalDeliveryId: true; + idempotencyKey: true; + runId: true; + rawBodyHash: true; + parsedEvent: true; + headers: true; + errorMessage: true; + filterReason: true; + createdAt: true; + updatedAt: true; + processedAt: true; + }; +}>; + +export type GetWebhookDeliveryOptions = { + organizationId: string; + projectId: string; + environmentId: string; + friendlyId: string; +}; + +export type GetDeliveriesByFriendlyIdsOptions = { + organizationId: string; + projectId: string; + environmentId: string; + friendlyIds: string[]; +}; + +export type CountDeliveriesByEndpointOptions = { + organizationId: string; + projectId: string; + environmentId: string; + webhookEndpointIds: string[]; + period: number; // lookback window in ms +}; + +export interface IWebhookDeliveriesRepository { + name: string; + listDeliveryIds(options: ListWebhookDeliveriesOptions): Promise; + listDeliveries(options: ListWebhookDeliveriesOptions): Promise<{ + deliveries: ListedWebhookDelivery[]; + pagination: { nextCursor: string | null; previousCursor: string | null }; + }>; + getDeliveriesByFriendlyIds( + options: GetDeliveriesByFriendlyIdsOptions + ): Promise; + countDeliveries(options: FilterWebhookDeliveriesOptions): Promise; + // One grouped query for many endpoints -> Map (avoids an N+1 of count queries). + countDeliveriesByEndpoint( + options: CountDeliveriesByEndpointOptions + ): Promise>; + getDelivery(options: GetWebhookDeliveryOptions): Promise; +} + +export class WebhookDeliveriesRepository implements IWebhookDeliveriesRepository { + private readonly clickHouseRepository: ClickHouseWebhookDeliveriesRepository; + + constructor(private readonly options: WebhookDeliveriesRepositoryOptions) { + this.clickHouseRepository = new ClickHouseWebhookDeliveriesRepository(options); + } + + get name() { + return this.clickHouseRepository.name; + } + + async listDeliveryIds(options: ListWebhookDeliveriesOptions) { + return startActiveSpan( + "webhookDeliveriesRepository.listDeliveryIds", + async () => this.clickHouseRepository.listDeliveryIds(options), + { + attributes: { + "repository.name": "clickhouse", + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); + } + + async listDeliveries(options: ListWebhookDeliveriesOptions) { + return startActiveSpan( + "webhookDeliveriesRepository.listDeliveries", + async () => this.clickHouseRepository.listDeliveries(options), + { + attributes: { + "repository.name": "clickhouse", + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); + } + + async getDeliveriesByFriendlyIds(options: GetDeliveriesByFriendlyIdsOptions) { + return startActiveSpan( + "webhookDeliveriesRepository.getDeliveriesByFriendlyIds", + async () => this.clickHouseRepository.getDeliveriesByFriendlyIds(options), + { + attributes: { + "repository.name": "clickhouse", + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); + } + + async countDeliveries(options: FilterWebhookDeliveriesOptions) { + return startActiveSpan( + "webhookDeliveriesRepository.countDeliveries", + async () => this.clickHouseRepository.countDeliveries(options), + { + attributes: { + "repository.name": "clickhouse", + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); + } + + async countDeliveriesByEndpoint(options: CountDeliveriesByEndpointOptions) { + return startActiveSpan( + "webhookDeliveriesRepository.countDeliveriesByEndpoint", + async () => this.clickHouseRepository.countDeliveriesByEndpoint(options), + { + attributes: { + "repository.name": "clickhouse", + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); + } + + async getDelivery(options: GetWebhookDeliveryOptions) { + return startActiveSpan( + "webhookDeliveriesRepository.getDelivery", + async () => this.clickHouseRepository.getDelivery(options), + { + attributes: { + "repository.name": "clickhouse", + organizationId: options.organizationId, + projectId: options.projectId, + environmentId: options.environmentId, + }, + } + ); + } +} + +// Inline-constructed per request (NOT a module-level singleton), like RunsRepository. +export function webhookDeliveriesRepository(opts: WebhookDeliveriesRepositoryOptions) { + return new WebhookDeliveriesRepository(opts); +} diff --git a/apps/webapp/app/services/webhookIngressIpRateLimit.server.ts b/apps/webapp/app/services/webhookIngressIpRateLimit.server.ts new file mode 100644 index 00000000000..02a22461338 --- /dev/null +++ b/apps/webapp/app/services/webhookIngressIpRateLimit.server.ts @@ -0,0 +1,26 @@ +import type { RequestHandler } from "express"; +import { Ratelimit } from "@upstash/ratelimit"; +import { env } from "~/env.server"; +import { RateLimiter, type Duration } from "./rateLimiter.server"; + +const ipLimiter = new RateLimiter({ + keyPrefix: "webhook-ingress-ip", + limiter: Ratelimit.fixedWindow( + env.WEBHOOK_INGRESS_IP_RATE_LIMIT_TOKENS, + env.WEBHOOK_INGRESS_IP_RATE_LIMIT_WINDOW as Duration + ), +}); + +// Coarse per-IP gate mounted in server.ts ahead of the Remix handler. The +// per-opaqueId limiter (webhookIngressRateLimit.server) is the real protection. +export const webhookIngressIpRateLimiter: RequestHandler = async (req, res, next) => { + if (!req.path.startsWith("/webhooks/v1/ingest/")) return next(); + const ip = + (req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() || req.ip || "unknown"; + const { success } = await ipLimiter.limit(ip); + if (!success) { + res.status(429).json({ error: "Too many requests" }); + return; + } + next(); +}; diff --git a/apps/webapp/app/services/webhookIngressRateLimit.server.ts b/apps/webapp/app/services/webhookIngressRateLimit.server.ts new file mode 100644 index 00000000000..a55e7e831d9 --- /dev/null +++ b/apps/webapp/app/services/webhookIngressRateLimit.server.ts @@ -0,0 +1,16 @@ +import { Ratelimit } from "@upstash/ratelimit"; +import { env } from "~/env.server"; +import { RateLimiter, type Duration } from "./rateLimiter.server"; + +// Per-opaqueId fixed-window limiter for the unauthenticated webhook ingress +// route. apiRateLimiter/engineRateLimiter only match /api and /engine and key +// off the auth header, so they never see this request; this is the real +// protection (the per-IP limiter in server.ts is coarse). +export const webhookIngressRateLimiter = new RateLimiter({ + keyPrefix: "webhook-ingress", + limiter: Ratelimit.fixedWindow( + env.WEBHOOK_INGRESS_RATE_LIMIT_TOKENS, + env.WEBHOOK_INGRESS_RATE_LIMIT_WINDOW as Duration + ), + logFailure: true, +}); diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 187bc50b549..887d20c3fa3 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -367,6 +367,52 @@ export function v3AgentTaskPath( )}`; } +export function v3WebhooksPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath +) { + // Top-level Webhooks section landing = the cross-endpoint deliveries list. + return `${v3EnvironmentPath(organization, project, environment)}/webhooks`; +} + +export function v3WebhookTaskPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath, + webhookSlug: string +) { + return `${v3EnvironmentPath(organization, project, environment)}/webhooks/${encodeURIComponent( + webhookSlug + )}`; +} + +export function v3WebhookDeliveryPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath, + deliveryFriendlyId: string +) { + return `${v3EnvironmentPath( + organization, + project, + environment + )}/webhooks/deliveries/${encodeURIComponent(deliveryFriendlyId)}`; +} + +export function v3WebhookEndpointPath( + organization: OrgForPath, + project: ProjectForPath, + environment: EnvironmentForPath, + endpointFriendlyId: string +) { + return `${v3EnvironmentPath( + organization, + project, + environment + )}/webhooks/endpoints/${encodeURIComponent(endpointFriendlyId)}`; +} + export function v3StandardTaskPath( organization: OrgForPath, project: ProjectForPath, diff --git a/apps/webapp/app/utils/readBodyWithCap.server.ts b/apps/webapp/app/utils/readBodyWithCap.server.ts new file mode 100644 index 00000000000..0c5a8657249 --- /dev/null +++ b/apps/webapp/app/utils/readBodyWithCap.server.ts @@ -0,0 +1,40 @@ +// Read a request body, aborting as soon as the accumulated bytes exceed `limitBytes`. A chunked +// upload can omit or understate Content-Length, so reading the stream incrementally (instead of +// request.arrayBuffer(), which buffers the whole stream first) caps the memory a single request can +// force us to hold before rejection. Returns null when the body is over the limit. +export async function readBodyWithCap( + request: Request, + limitBytes: number +): Promise { + if (!request.body) { + return new Uint8Array(0); + } + + const reader = request.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + if (!value) continue; + total += value.byteLength; + if (total > limitBytes) { + await reader.cancel(); + return null; + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + + const out = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + out.set(chunk, offset); + offset += chunk.byteLength; + } + return out; +} diff --git a/apps/webapp/app/utils/webhookIngressUrl.server.ts b/apps/webapp/app/utils/webhookIngressUrl.server.ts new file mode 100644 index 00000000000..8789fd7a2b3 --- /dev/null +++ b/apps/webapp/app/utils/webhookIngressUrl.server.ts @@ -0,0 +1,11 @@ +import { env } from "~/env.server"; + +// Public origin webhook providers POST to. A dedicated WEBHOOK_INGRESS_ORIGIN (e.g. +// https://webhook.trigger.dev) takes precedence; otherwise it rides the API/app origin. +export function webhookIngressOrigin(): string { + return env.WEBHOOK_INGRESS_ORIGIN ?? env.API_ORIGIN ?? env.APP_ORIGIN; +} + +export function webhookIngressUrl(opaqueId: string): string { + return `${webhookIngressOrigin()}/webhooks/v1/ingest/${opaqueId}`; +} diff --git a/apps/webapp/app/v3/featureFlags.ts b/apps/webapp/app/v3/featureFlags.ts index 49a18eaf35f..b8699301f0c 100644 --- a/apps/webapp/app/v3/featureFlags.ts +++ b/apps/webapp/app/v3/featureFlags.ts @@ -5,6 +5,7 @@ export const FEATURE_FLAG = { taskEventRepository: "taskEventRepository", hasQueryAccess: "hasQueryAccess", hasLogsPageAccess: "hasLogsPageAccess", + hasWebhooksAccess: "hasWebhooksAccess", hasAiAccess: "hasAiAccess", hasDashboardAgentAccess: "hasDashboardAgentAccess", hasComputeAccess: "hasComputeAccess", @@ -29,6 +30,7 @@ export const FeatureFlagCatalog = { [FEATURE_FLAG.taskEventRepository]: z.enum(["clickhouse", "clickhouse_v2", "postgres"]), [FEATURE_FLAG.hasQueryAccess]: z.coerce.boolean(), [FEATURE_FLAG.hasLogsPageAccess]: z.coerce.boolean(), + [FEATURE_FLAG.hasWebhooksAccess]: z.coerce.boolean(), [FEATURE_FLAG.hasAiAccess]: z.coerce.boolean(), // Gates the in-dashboard AI agent panel. Controllable globally and per-org // (org wins). Defaults off via DASHBOARD_AGENT_ENABLED. diff --git a/apps/webapp/app/v3/services/adminWorker.server.ts b/apps/webapp/app/v3/services/adminWorker.server.ts index 562e4f451dc..1fa7aa669ef 100644 --- a/apps/webapp/app/v3/services/adminWorker.server.ts +++ b/apps/webapp/app/v3/services/adminWorker.server.ts @@ -16,6 +16,11 @@ import { runsReplicationInstance } from "~/services/runsReplicationInstance.serv // effect the bundler must preserve. See TRI-9864. import { sessionsReplicationInstance } from "~/services/sessionsReplicationInstance.server"; (globalThis as Record).__sessionsReplicationInstance = sessionsReplicationInstance; +// Same reference-hold as the sessions replicator above (and the same +// `void`-tree-shaking caveat) for the webhook deliveries replication singleton. +import { webhookDeliveriesReplicationInstance } from "~/services/webhookDeliveriesReplicationInstance.server"; +(globalThis as Record).__webhookDeliveriesReplicationInstance = + webhookDeliveriesReplicationInstance; import { singleton } from "~/utils/singleton"; import { tracer } from "../tracer.server"; import { $replica } from "~/db.server"; diff --git a/apps/webapp/app/v3/services/changeCurrentDeployment.server.ts b/apps/webapp/app/v3/services/changeCurrentDeployment.server.ts index 9782d4d8cb7..d09b068f030 100644 --- a/apps/webapp/app/v3/services/changeCurrentDeployment.server.ts +++ b/apps/webapp/app/v3/services/changeCurrentDeployment.server.ts @@ -1,6 +1,7 @@ import { BackgroundWorkerMetadata, tryCatch } from "@trigger.dev/core/v3"; import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction, WorkerDeployment } from "@trigger.dev/database"; +import { webhookPrisma } from "~/db.server"; import { logger } from "~/services/logger.server"; import { syncTaskIdentifiers } from "~/services/taskIdentifierRegistry.server"; import { @@ -9,7 +10,7 @@ import { } from "~/services/taskMetadataCache.server"; import { taskMetadataCacheInstance } from "~/services/taskMetadataCacheInstance.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; -import { syncDeclarativeSchedules } from "./createBackgroundWorker.server"; +import { syncDeclarativeSchedules, syncDeclarativeWebhooks } from "./createBackgroundWorker.server"; import { compareDeploymentVersions } from "../utils/deploymentVersions"; export type ChangeCurrentDeploymentDirection = "promote" | "rollback"; @@ -217,5 +218,12 @@ export class ChangeCurrentDeploymentService extends BaseService { } await syncDeclarativeSchedules(parsed.data.tasks, worker, environment, this._prisma); + await syncDeclarativeWebhooks( + parsed.data.webhooks ?? [], + worker, + environment, + this._prisma, + webhookPrisma + ); } } diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index 43551e849f0..b761cd1123c 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -2,16 +2,24 @@ import type { BackgroundWorkerMetadata, BackgroundWorkerSourceFileMetadata, CreateBackgroundWorkerRequestBody, + FilterAst, PromptResource, QueueManifest, TaskResource, + WebhookResource, } from "@trigger.dev/core/v3"; -import { tryCatch } from "@trigger.dev/core/v3"; -import { BackgroundWorkerId, stringifyDuration } from "@trigger.dev/core/v3/isomorphic"; +import { FILTER_AST_VERSION, tryCatch } from "@trigger.dev/core/v3"; +import { FilterParseError, parseFilter } from "@internal/webhook-engine"; +import { + BackgroundWorkerId, + WebhookEndpointId, + stringifyDuration, +} from "@trigger.dev/core/v3/isomorphic"; +import { randomBytes } from "node:crypto"; import type { BackgroundWorker, TaskQueue, TaskQueueType } from "@trigger.dev/database"; import cronstrue from "cronstrue"; -import type { PrismaClientOrTransaction } from "~/db.server"; -import { $transaction, Prisma } from "~/db.server"; +import type { PrismaClientOrTransaction, WebhookDatabase } from "~/db.server"; +import { $transaction, Prisma, webhookPrisma } from "~/db.server"; import { sanitizeQueueName } from "~/models/taskQueue.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; @@ -199,6 +207,24 @@ export class CreateBackgroundWorkerService extends BaseService { throw new ServiceValidationError("Error syncing declarative schedules"); } + const [webhooksError] = await tryCatch( + syncDeclarativeWebhooks( + body.metadata.webhooks ?? [], + backgroundWorker, + environment, + this._prisma, + webhookPrisma + ) + ); + if (webhooksError) { + logger.error("Error syncing declarative webhooks", { + error: webhooksError, + backgroundWorker, + environment, + }); + if (webhooksError instanceof ServiceValidationError) throw webhooksError; + } + const [syncIdentifiersError] = await tryCatch( syncTaskIdentifiers( environment.id, @@ -353,7 +379,7 @@ async function createWorkerTask( ): Promise { // Hoisted so the P2002 catch branch can return the same entry shape. let queue: TaskQueue | undefined; - let resolvedTriggerSource: "SCHEDULED" | "AGENT" | "STANDARD" | undefined; + let resolvedTriggerSource: "SCHEDULED" | "AGENT" | "WEBHOOK" | "STANDARD" | undefined; let resolvedTtl: string | null | undefined; try { @@ -379,7 +405,9 @@ async function createWorkerTask( ? ("SCHEDULED" as const) : task.triggerSource === "agent" ? ("AGENT" as const) - : ("STANDARD" as const); + : task.triggerSource === "webhook" + ? ("WEBHOOK" as const) + : ("STANDARD" as const); resolvedTtl = typeof task.ttl === "number" ? (stringifyDuration(task.ttl) ?? null) : (task.ttl ?? null); @@ -639,6 +667,133 @@ export class CreateDeclarativeScheduleError extends Error { } } +// TODO: centralize (the P2 dynamic webhooks.create() API will also mint opaqueIds). +function generateOpaqueId(): string { + return randomBytes(16).toString("base64url"); +} + +export async function syncDeclarativeWebhooks( + webhooks: WebhookResource[], + worker: BackgroundWorker, + environment: AuthenticatedEnvironment, + prisma: PrismaClientOrTransaction, + // Endpoint rows live on the webhook DB; the task-existence check below stays on the main client. + webhookPrisma: WebhookDatabase +) { + const existing = await webhookPrisma.webhookEndpoint.findMany({ + where: { + runtimeEnvironmentId: environment.id, + endpointTenantId: "", + endpointExternalRef: "", + }, + }); + const missing = new Set(existing.map((e) => e.handlerWebhookId)); + + for (const wh of webhooks) { + // Both routing targets resolve to a task: a fan-out webhook to its own task, a session webhook to + // the claiming agent. Validate the target exists in this worker so a bad route fails at sync. + const targetTaskSlug = + wh.routingTarget.type === "task" ? wh.routingTarget.taskId : wh.routingTarget.taskIdentifier; + const taskExists = await prisma.backgroundWorkerTask.findFirst({ + where: { workerId: worker.id, slug: targetTaskSlug }, + select: { id: true }, + }); + if (!taskExists) { + throw new ServiceValidationError( + `Webhook "${wh.id}" routes to unknown task "${targetTaskSlug}"` + ); + } + + missing.delete(wh.id); + + // Compile `filter` into a FilterAst, once here at sync. A bad filter fails the deploy with a clear + // message rather than surfacing at ingest. Re-deploying without a filter nulls the columns. + let filterNode: FilterAst | undefined; + if (wh.filter) { + try { + filterNode = parseFilter(wh.filter); + } catch (error) { + if (error instanceof FilterParseError) { + throw new ServiceValidationError( + `Webhook "${wh.id}" has an invalid filter: ${error.message}` + ); + } + throw error; + } + } + const filterData = { + filter: wh.filter ?? null, + filterAst: filterNode ? (filterNode as unknown as Prisma.InputJsonValue) : Prisma.DbNull, + filterAstVersion: filterNode ? FILTER_AST_VERSION : null, + }; + + // Validate a session target's startOn like the route filter: a bad predicate fails the deploy, not ingest. + if (wh.routingTarget.type === "session" && wh.routingTarget.startOn) { + try { + parseFilter(wh.routingTarget.startOn); + } catch (error) { + if (error instanceof FilterParseError) { + throw new ServiceValidationError( + `Webhook "${wh.id}" has an invalid startOn: ${error.message}` + ); + } + throw error; + } + } + + const found = existing.find((e) => e.handlerWebhookId === wh.id); + if (found) { + await webhookPrisma.webhookEndpoint.update({ + where: { id: found.id }, + data: { + source: wh.source, + routingTarget: wh.routingTarget as unknown as Prisma.InputJsonValue, + verifierArtifact: wh.verifierArtifact as unknown as Prisma.InputJsonValue, + secretProvisioning: wh.secretProvisioning ?? "either", + metadata: (wh.metadata ?? {}) as unknown as Prisma.InputJsonValue, + status: "ACTIVE", + ...filterData, + }, + }); + } else { + const { id, friendlyId } = WebhookEndpointId.generate(); + await webhookPrisma.webhookEndpoint.create({ + data: { + id, + friendlyId, + opaqueId: generateOpaqueId(), // CSPRNG, NOT a friendlyId + organizationId: environment.organizationId, + projectId: environment.projectId, + runtimeEnvironmentId: environment.id, + environmentType: environment.type, + endpointTenantId: "", + endpointExternalRef: "", + source: wh.source, + handlerWebhookId: wh.id, + routingTarget: wh.routingTarget as unknown as Prisma.InputJsonValue, + verifierArtifact: wh.verifierArtifact as unknown as Prisma.InputJsonValue, + secretProvisioning: wh.secretProvisioning ?? "either", + metadata: (wh.metadata ?? {}) as unknown as Prisma.InputJsonValue, + status: "ACTIVE", + ...filterData, + }, + }); + } + } + + if (missing.size > 0) { + await webhookPrisma.webhookEndpoint.updateMany({ + where: { + runtimeEnvironmentId: environment.id, + endpointTenantId: "", + endpointExternalRef: "", + handlerWebhookId: { in: Array.from(missing) }, + }, + data: { status: "INACTIVE" }, + }); + } +} + export async function syncDeclarativeSchedules( tasks: TaskResource[], worker: BackgroundWorker, diff --git a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts index 66a17bb9f65..1a7266e0b33 100644 --- a/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts +++ b/apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts @@ -13,11 +13,13 @@ import { createBackgroundFiles, createWorkerResources, syncDeclarativeSchedules, + syncDeclarativeWebhooks, } from "./createBackgroundWorker.server"; import { findOrCreateBackgroundWorker } from "./createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server"; import { TimeoutDeploymentService } from "./timeoutDeployment.server"; import { recordDeploymentOutcome } from "./recordDeploymentOutcome.server"; import { env } from "~/env.server"; +import { webhookPrisma } from "~/db.server"; export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService { private readonly _taskMetaCache: TaskMetadataCache; @@ -228,6 +230,24 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService { throw serviceError; } + const [webhooksError] = await tryCatch( + syncDeclarativeWebhooks( + body.metadata.webhooks ?? [], + backgroundWorker, + environment, + this._prisma, + webhookPrisma + ) + ); + + if (webhooksError) { + logger.error("Error syncing declarative webhooks", { error: webhooksError }); + if (webhooksError instanceof ServiceValidationError) { + await this.#failBackgroundWorkerDeployment(deployment, webhooksError, environment); + throw webhooksError; + } + } + // Guarded BUILDING → DEPLOYING transition. `updateMany` for optimistic concurrency control const { count: updatedCount } = await this._prisma.workerDeployment.updateMany({ where: { diff --git a/apps/webapp/app/v3/webhookEngine.server.ts b/apps/webapp/app/v3/webhookEngine.server.ts new file mode 100644 index 00000000000..97dda0fd736 --- /dev/null +++ b/apps/webapp/app/v3/webhookEngine.server.ts @@ -0,0 +1,284 @@ +import { WebhookEngine } from "@internal/webhook-engine"; +import type { WebhookDeliverTaskErrorType } from "@internal/webhook-engine"; +import { tryCatch } from "@trigger.dev/core/utils"; +import { z } from "zod"; +import { prisma, webhookPrisma } from "~/db.server"; +import { env } from "~/env.server"; +import { findEnvironmentById } from "~/models/runtimeEnvironment.server"; +import { logger } from "~/services/logger.server"; +import { S2RealtimeStreams } from "~/services/realtime/s2realtimeStreams.server"; +import { + ensureRunForSession, + type SessionTriggerConfig, +} from "~/services/realtime/sessionRunManager.server"; +import { findOrCreateSession, findSessionByExternalId } from "~/services/realtime/sessions.server"; +import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server"; +import { + claimSessionStreamPart, + drainSessionStreamWaitpoints, + releaseSessionStreamPart, +} from "~/services/sessionStreamWaitpointCache.server"; +import { getSecretStore } from "~/services/secrets/secretStore.server"; +import { singleton } from "~/utils/singleton"; +import { engine as runEngine } from "./runEngine.server"; +import { ServiceValidationError } from "./services/common.server"; +import { TriggerTaskService } from "./services/triggerTask.server"; +import { meter, tracer } from "./tracer.server"; + +export const webhookEngine = singleton("WebhookEngine", createWebhookEngine); + +export type { WebhookEngine }; + +// The plaintext signing secret is stored under the "DATABASE" SecretStore +// provider as { secret: string } (same shape as environment variables). +const SigningSecretSchema = z.object({ secret: z.string() }); + +function createWebhookEngine() { + // The engine owns the webhook tables, so it runs on the webhook DB client. The signing-secret + // store stays on the main client below (SecretStore is control-plane, not part of the split). + const secretStore = getSecretStore("DATABASE", { prismaClient: prisma }); + + const engine = new WebhookEngine({ + prisma: webhookPrisma, + logLevel: env.WEBHOOK_ENGINE_LOG_LEVEL, + redis: { + host: env.WEBHOOK_WORKER_REDIS_HOST ?? "localhost", + port: env.WEBHOOK_WORKER_REDIS_PORT ?? 6379, + username: env.WEBHOOK_WORKER_REDIS_USERNAME, + password: env.WEBHOOK_WORKER_REDIS_PASSWORD, + keyPrefix: "webhook:", + enableAutoPipelining: true, + ...(env.WEBHOOK_WORKER_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), + }, + worker: { + concurrency: env.WEBHOOK_WORKER_CONCURRENCY_LIMIT, + workers: env.WEBHOOK_WORKER_CONCURRENCY_WORKERS, + tasksPerWorker: env.WEBHOOK_WORKER_CONCURRENCY_TASKS_PER_WORKER, + pollIntervalMs: env.WEBHOOK_WORKER_POLL_INTERVAL, + shutdownTimeoutMs: env.WEBHOOK_WORKER_SHUTDOWN_TIMEOUT_MS, + disabled: env.WEBHOOK_WORKER_ENABLED === "0", + }, + partitions: { + ensureSchedule: env.WEBHOOK_PARTITION_ENSURE_SCHEDULE, + ensureJitterInMs: env.WEBHOOK_PARTITION_ENSURE_JITTER_MS, + lookaheadDays: env.WEBHOOK_PARTITION_LOOKAHEAD_DAYS, + retentionDays: env.WEBHOOK_PARTITION_RETENTION_DAYS, + }, + frontGate: { + defaultTtlSeconds: env.WEBHOOK_FRONT_GATE_DEFAULT_TTL_SECONDS, + maxTtlSeconds: env.WEBHOOK_FRONT_GATE_MAX_TTL_SECONDS, + }, + endpointCache: { + ttlMs: env.WEBHOOK_ENDPOINT_CACHE_TTL_MS, + maxSize: env.WEBHOOK_ENDPOINT_CACHE_MAX_SIZE, + }, + tracer, + meter, + resolveSigningSecret: async (key) => { + const value = await secretStore.getSecret(SigningSecretSchema, key); + // Fail closed: an unset/empty secret returns undefined so ingest rejects. + return value?.secret || undefined; + }, + triggerTask: async ({ + environmentId, + taskId, + idempotencyKey, + idempotencyKeyExpiresAt, + payload, + headers, + identityTags, + endpointMetadata, + }) => { + try { + const environment = await findEnvironmentById(environmentId); + if (!environment) { + return { success: false, errorType: "NOT_FOUND", error: "Environment not found" }; + } + + const triggerService = new TriggerTaskService(); + + const result = await triggerService.call( + taskId, + environment, + { + // The webhook task run receives a { event, headers } envelope; the SDK's webhook() + // run unwraps it into onEvent({ event, headers }). + payload: { event: payload, headers }, + options: { + tags: identityTags, + metadata: (endpointMetadata as Record) ?? undefined, + }, + }, + { + idempotencyKey, + idempotencyKeyExpiresAt, + triggerSource: "webhook", + triggerAction: "trigger", + customIcon: "webhook", + } + ); + + return { success: !!result, runId: result?.run.id }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + let errorType: WebhookDeliverTaskErrorType = "SYSTEM_ERROR"; + + if ( + error instanceof ServiceValidationError && + errorMessage.includes("queue size limit for this environment has been reached") + ) { + errorType = "QUEUE_LIMIT"; + } + + return { success: false, error: errorMessage, errorType }; + } + }, + // Route a verified delivery to a session: find-or-create it, then append a webhook action to `.in`. + // The run boots on a preload payload (so onChatStart fires), then reads the action from `.in`. + deliverToSession: async ({ + environmentId, + taskIdentifier, + externalId, + deliverAs, + actionType, + connectorId, + event, + source, + headers, + deliveryId, + triggerConfigTemplate, + isSessionStart, + }) => { + try { + const environment = await findEnvironmentById(environmentId); + if (!environment) { + return { success: false, errorType: "NOT_FOUND", error: "Environment not found" }; + } + + const template = (triggerConfigTemplate ?? {}) as Partial; + const triggerConfig: SessionTriggerConfig = { + ...template, + basePayload: { + messages: [], + trigger: "preload", + chatId: externalId, + ...(template.basePayload ?? {}), + }, + }; + + // Resume an existing session; otherwise only START one when the event is a session-start + // (startOn). Resume-only with no session yet -> ignore (no session, no run, no egress). + const existing = await findSessionByExternalId(environment, externalId); + if (!existing && !isSessionStart) { + return { + success: true, + skipped: true, + skippedReason: "startOn: not a session-start event", + }; + } + const { session, isCached } = existing + ? { session: existing, isCached: true } + : await findOrCreateSession({ + environment, + externalId, + type: "chat.agent", + taskIdentifier, + triggerConfig, + }); + + if (session.closedAt || (session.expiresAt && session.expiresAt.getTime() < Date.now())) { + return { success: false, error: "Session is closed or expired" }; + } + + // Boot / revive the run, then append the action. The run reads it from `.in`. + const ensureResult = await ensureRunForSession({ + session, + environment, + reason: isCached ? "continuation" : "initial", + }); + + const realtimeStream = getRealtimeStreamInstance(environment, "v2", { session }); + if (!(realtimeStream instanceof S2RealtimeStreams)) { + return { success: false, error: "Session channels require the S2 realtime backend" }; + } + + const addressingKey = session.externalId ?? session.friendlyId; + // "action" (chat.event) -> onAction envelope; "message" (channels) -> a turn whose message the + // run derives by applying the connector's inbound() to the raw event. + const payload = + deliverAs === "message" + ? { + chatId: externalId, + trigger: "submit-message", + channelEvent: { connectorId, event, source, headers, deliveryId }, + } + : { + chatId: externalId, + trigger: "action", + actionSource: "webhook", + action: { type: actionType, event, source, headers, deliveryId }, + }; + const part = JSON.stringify({ kind: "message", payload }); + + // deliveryId as the part id → a deliver-job retry re-claims the same id and skips a duplicate + // append. The S2 record is durable, so a run that boots later still reads it. + const wonClaim = await claimSessionStreamPart( + environment.id, + addressingKey, + "in", + deliveryId + ); + if (wonClaim) { + const [appendError] = await tryCatch( + realtimeStream.appendPartToSessionStream(part, deliveryId, addressingKey, "in") + ); + if (appendError) { + // Nothing landed — release the claim so a retry re-appends the same id. + await releaseSessionStreamPart(environment.id, addressingKey, "in", deliveryId); + // A ServiceValidationError (e.g. record too large) is terminal; anything else is transient. + if (appendError instanceof ServiceValidationError) { + return { success: false, error: appendError.message }; + } + throw appendError; + } + } + + // Wake any `.in` waitpoints the run registered (best-effort; the record is durable in S2). + const [drainError, waitpointIds] = await tryCatch( + drainSessionStreamWaitpoints(environment.id, addressingKey, "in") + ); + if (drainError) { + logger.error("deliverToSession: failed to drain session waitpoints", { + externalId, + error: drainError, + }); + } else if (waitpointIds && waitpointIds.length > 0) { + await Promise.all( + waitpointIds.map((waitpointId) => + tryCatch( + runEngine.completeWaitpoint({ + id: waitpointId, + output: { value: part, type: "application/json", isError: false }, + }) + ) + ) + ); + } + + return { success: true, runId: ensureResult.runId }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + let errorType: WebhookDeliverTaskErrorType = "SYSTEM_ERROR"; + if ( + error instanceof ServiceValidationError && + errorMessage.includes("queue size limit for this environment has been reached") + ) { + errorType = "QUEUE_LIMIT"; + } + return { success: false, error: errorMessage, errorType }; + } + }, + }); + + return engine; +} diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 678eeb76033..99504c9d9c7 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -62,6 +62,8 @@ "@internal/schedule-engine": "workspace:*", "@internal/tracing": "workspace:*", "@internal/tsql": "workspace:*", + "@internal/webhook-engine": "workspace:*", + "@internal/webhook-sources": "workspace:*", "@internationalized/date": "^3.5.1", "@jsonhero/schema-infer": "^0.1.5", "@kapaai/react-sdk": "^0.1.3", diff --git a/apps/webapp/server.ts b/apps/webapp/server.ts index a5dbfb06b2a..7f9244416a4 100644 --- a/apps/webapp/server.ts +++ b/apps/webapp/server.ts @@ -145,8 +145,18 @@ async function startServer() { // log dominates log volume. HTTP_ACCESS_LOG_DISABLED suppresses successful // (2xx) access logs; non-2xx responses are always logged so errors stay visible. const suppressSuccessfulAccessLogs = process.env.HTTP_ACCESS_LOG_DISABLED === "1"; + // Strip the query string from webhook ingress URLs (they may carry a + // url-secret) before they reach the access log. Other paths pass through. + morgan.token("url-redacted", (req: any) => { + const url: string = req.originalUrl ?? req.url ?? ""; + if (url.startsWith("/webhooks/v1/ingest/")) { + const q = url.indexOf("?"); + return q === -1 ? url : url.slice(0, q); + } + return url; + }); app.use( - morgan("tiny", { + morgan(":method :url-redacted :status :res[content-length] - :response-time ms", { skip: (_req, res) => suppressSuccessfulAccessLogs && res.statusCode >= 200 && res.statusCode < 300, }) @@ -187,6 +197,8 @@ async function startServer() { const otlpRateLimiter: RequestHandler = build.entry.module.otlpRateLimiter; const runWithHttpContext: RunWithHttpContextFunction = build.entry.module.runWithHttpContext; const tenantContextMiddleware: RequestHandler = build.entry.module.tenantContextMiddleware; + const webhookIngressIpRateLimiter: RequestHandler = + build.entry.module.webhookIngressIpRateLimiter; app.use((req, res, next) => { // helpful headers: @@ -237,6 +249,7 @@ async function startServer() { app.use(apiRateLimiter); app.use(engineRateLimiter); app.use(otlpRateLimiter); + app.use(webhookIngressIpRateLimiter); app.use(tenantContextMiddleware); diff --git a/apps/webapp/tailwind.config.js b/apps/webapp/tailwind.config.js new file mode 100644 index 00000000000..43f16718a30 --- /dev/null +++ b/apps/webapp/tailwind.config.js @@ -0,0 +1,387 @@ +/** @type {import('tailwindcss').Config} */ +const colors = require("tailwindcss/colors"); + +// V2 +const slate = { + 450: "#7E8FA6", + 500: "#6B7C95", + 550: "#586981", + 600: "#45566D", + 650: "#3C4B62", + 750: "#293649", + 850: "#1A2434", + 900: "#131B2B", + 950: "#0E1521", + 1000: "#0B1018", +}; + +const toxic = { + 50: "#E3FFE6", + 100: "#C8FFCD", + 200: "#A9FFAB", + 300: "#8AFF96", + 400: "#6DFC7B", + 500: "#41FF54", + 600: "#28F03C", + 700: "#2AE03C", + 800: "#22D834", + 900: "#16CC28", +}; + +const acid = { + 50: "#F9FFD1", + 100: "#F6FFB6", + 200: "#F3FF99", + 300: "#EEFF82", + 400: "#E7FF52", + 500: "#DAF437", + 600: "#C5E118", + 700: "#B2CD0A", + 800: "#A5BE07", + 900: "#9FB802", +}; + +const midnight = { + ...colors.slate, + 450: colors.slate[850], + 500: colors.slate[650], + 550: colors.slate[700], + 600: colors.slate[750], + 650: colors.slate[800], + 750: colors.slate[850], + 800: colors.slate[900], + 850: "#0E1521", + 900: "#0B1018", +}; + +// V3 +const charcoal = { + 100: "#E8E9EC", + 200: "#D7D9DD", + 300: "#B5B8C0", + 400: "#878C99", + 500: "#5F6570", + 550: "#4D525B", + 600: "#3B3E45", + 650: "#2C3034", + 700: "#272A2E", + 750: "#212327", + 775: "#1C1E21", + 800: "#1A1B1F", + 850: "#15171A", + 900: "#121317", + 950: "#0D0E12", + 1000: "#0B0C0F", +}; + +const apple = { + 100: "#E4FFC9", + 200: "#CFFFA0", + 300: "#BFFF81", + 400: "#AFFF62", + 500: "#A8FF53", + 600: "#82D134", + 700: "#6FB12F", + 750: "#5E932A", + 800: "#45711A", + 850: "#2E4E10", + 900: "#20370A", + 950: "#152506", +}; + +const mint = { + 50: "#F0FDF4", + 100: "#DDFBE6", + 200: "#BDF5D0", + 300: "#87EBA9", + 400: "#4FD97E", + 500: "#28BF5C", + 600: "#1B9E48", + 700: "#197C3C", + 800: "#196233", + 900: "#16512C", + 950: "#062D15", +}; + +const sun = { + 50: "#FDFEE8", + 100: "#FDFFC2", + 200: "#FFFF89", + 300: "#FFF852", + 400: "#FDEA12", + 500: "#ECCF06", + 600: "#CCA302", + 700: "#A37505", + 800: "#865B0D", + 900: "#724B11", + 950: "#432705", +}; + +const lavender = { + 50: "##f4f2ff", + 100: "#eae8ff", + 200: "#d7d4ff", + 300: "#bab2ff", + 400: "#826dff", + 500: "#7655fd", + 600: "#6532f5", + 700: "#5620e1", + 800: "#481abd", + 900: "#3d189a", + 950: "#230c69", +}; + +/** Trigger.dev custom palette */ + +/** Text colors */ +const primary = apple[500]; +const secondary = charcoal[650]; +const tertiary = charcoal[700]; +const textLink = lavender[400]; +const textDimmed = charcoal[400]; +const textBright = charcoal[200]; +const backgroundBright = charcoal[800]; +const backgroundDimmed = charcoal[850]; +const gridBright = charcoal[700]; +const gridDimmed = charcoal[750]; +const success = mint[500]; +const pending = colors.blue[500]; +const warning = colors.amber[500]; +const error = colors.rose[600]; +const devEnv = colors.pink[500]; +const stagingEnv = colors.orange[400]; +const previewEnv = colors.yellow[400]; +const prodEnv = mint[500]; + +/** Icon colors */ +const tasks = colors.blue[500]; +const runs = colors.indigo[500]; +const batches = colors.pink[500]; +const schedules = colors.yellow[500]; +const queues = colors.purple[500]; +const query = colors.blue[500]; +const metrics = colors.green[500]; +const customDashboards = charcoal[400]; +const deployments = colors.blue[500]; +const concurrency = colors.amber[500]; +const limits = colors.purple[500]; +const regions = colors.green[500]; +const logs = colors.pink[500]; +const tests = colors.pink[500]; +const apiKeys = colors.amber[500]; +const environmentVariables = colors.pink[500]; +const alerts = colors.red[500]; +const projectSettings = colors.blue[500]; +const orgSettings = colors.blue[500]; +const docs = colors.blue[500]; +const bulkActions = colors.emerald[500]; +const aiPrompts = colors.blue[500]; +const aiMetrics = colors.green[500]; +const errors = colors.amber[500]; +const agents = colors.purple[500]; +const webhooks = colors.teal[500]; // distinct from tasks(blue)/schedules(yellow)/agents(purple) +const sessions = colors.pink[500]; +const playgrounds = colors.fuchsia[500]; +const models = colors.violet[500]; +const previewBranches = colors.yellow[500]; + +/** Other variables */ +const radius = "0.5rem"; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./app/**/*.{ts,jsx,tsx}", "./node_modules/streamdown/dist/**/*.js"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + fontFamily: { + sans: ["Geist Variable", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"], + mono: ["Geist Mono Variable", "monaco", "Consolas", "Lucida Console", "monospace"], + }, + fontSize: { + xxs: [ + "0.65rem", + { + lineHeight: "0.75rem", + letterSpacing: "-0.01em", + fontWeight: "500", + }, + ], + "2sm": [ + "0.8125rem", + { + lineHeight: "0.875rem", + letterSpacing: "-0.01em", + fontWeight: "500", + }, + ], + }, + colors: { + charcoal, + apple, + lavender, + mint, + sun, + primary, + secondary, + tertiary, + "text-link": textLink, + "text-dimmed": textDimmed, + "text-bright": textBright, + "background-bright": backgroundBright, + "background-dimmed": backgroundDimmed, + "grid-bright": gridBright, + "grid-dimmed": gridDimmed, + success, + pending, + warning, + error, + dev: devEnv, + staging: stagingEnv, + prod: prodEnv, + preview: previewEnv, + tasks, + runs, + batches, + schedules, + concurrency, + queues, + query, + regions, + limits, + deployments, + logs, + tests, + apiKeys, + environmentVariables, + alerts, + projectSettings, + orgSettings, + docs, + bulkActions, + metrics, + customDashboards, + aiPrompts, + aiMetrics, + errors, + agents, + webhooks, + sessions, + playgrounds, + models, + previewBranches, + // shadcn/ui color tokens used by streamdown's internal components + // (link safety modal, code block actions, etc.) + // Values are defined via CSS variables in .streamdown-container + background: "hsl(var(--background, 230 16% 9%) / )", + foreground: "hsl(var(--foreground, 215 19% 87%) / )", + muted: { + DEFAULT: "hsl(var(--muted, 220 8% 17%) / )", + foreground: "hsl(var(--muted-foreground, 220 8% 57%) / )", + }, + border: "hsl(var(--border, 216 7% 27%) / )", + sidebar: "hsl(var(--sidebar, 228 10% 11%) / )", + "primary-foreground": "hsl(var(--primary-foreground, 230 16% 9%) / )", + }, + focusStyles: { + outline: "1px solid", + outlineOffset: "0px", + outlineColor: textLink, + borderRadius: "3px", + }, + borderRadius: { + lg: radius, + md: `calc(${radius} - 2px)`, + sm: `calc(${radius} - 4px)`, + }, + boxShadow: { + "glow-primary": "0 0 10px 5px rgba(218, 244, 55, 0.2)", + "glow-secondary": "0 0 10px 5px rgba(79, 70, 229, 0.2)", + "glow-pink": "0 0 10px 5px rgba(236, 72, 153, 0.2)", + }, + outlineWidth: { + 3: "3px", + }, + textShadow: { + custom: "1px 1px 1px rgba(0, 0, 0, 0.5)", // Offset-X | Offset-Y | Blur radius | Color + }, + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: 0 }, + }, + float: { + "0%": { transform: "translatey(0px)" }, + "50%": { transform: "translatey(7px)" }, + "100%": { transform: "translatey(0px)" }, + }, + "tile-move": { + "0%": { "background-position": "0px" }, + "100%": { "background-position": "8px" }, + }, + "tile-move-offset": { + "0%": { "background-position": "-1px" }, + "100%": { "background-position": "7px" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "tile-scroll": "tile-move 0.5s infinite linear", + "tile-scroll-offset": "tile-move-offset 0.5s infinite linear", + }, + backgroundImage: { + "gradient-radial": "radial-gradient(closest-side, var(--tw-gradient-stops))", + "gradient-primary": `linear-gradient(90deg, acid-500 0%, toxic-500 100%)`, + "gradient-primary-hover": `linear-gradient(80deg, acid-600 0%, toxic-600 100%)`, + "gradient-secondary": `linear-gradient(90deg, hsl(271 91 65) 0%, hsl(221 83 53) 100%)`, + "gradient-radial-secondary ": `radial-gradient(hsl(271 91 65), hsl(221 83 53))`, + }, + gridTemplateColumns: { + carousel: "repeat(6, 200px)", + }, + screens: { + "lg-height": { raw: "(max-height: 750px)" }, + "md-height": { raw: "(max-height: 600px)" }, + }, + width: { + 0.75: "0.1875rem", + 4.5: "1.125rem", + }, + height: { + 0.75: "0.1875rem", + 4.5: "1.125rem", + }, + size: { + 4.5: "1.125rem", + }, + }, + }, + plugins: [ + require("@tailwindcss/container-queries"), + require("@tailwindcss/forms"), + require("@tailwindcss/typography"), + require("tailwindcss-animate"), + require("tailwind-scrollbar"), + require("tailwind-scrollbar-hide"), + require("tailwindcss-textshadow"), + function ({ addUtilities, theme }) { + const focusStyles = theme("focusStyles", {}); + addUtilities({ + ".focus-custom": { + "&:focus-visible": focusStyles, + }, + }); + }, + ], +}; diff --git a/apps/webapp/test/components/webhookDeliveries/buildDeliveryTimelineItems.test.ts b/apps/webapp/test/components/webhookDeliveries/buildDeliveryTimelineItems.test.ts new file mode 100644 index 00000000000..f51a4a4f5a9 --- /dev/null +++ b/apps/webapp/test/components/webhookDeliveries/buildDeliveryTimelineItems.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; +import { + buildDeliveryTimelineItems, + type BuildDeliveryTimelineInput, +} from "~/components/webhookDeliveries/v1/buildDeliveryTimelineItems"; + +const createdAt = new Date("2026-07-13T10:32:04.120Z"); +const processedAt = new Date("2026-07-13T10:32:05.320Z"); + +function baseDelivery(overrides: Partial): BuildDeliveryTimelineInput { + return { + status: "PENDING", + createdAt, + processedAt: null, + errorMessage: null, + filterReason: null, + run: null, + session: null, + ...overrides, + }; +} + +describe("buildDeliveryTimelineItems", () => { + it("PENDING renders Received + an in-flight routing line, no terminal node", () => { + const items = buildDeliveryTimelineItems(baseDelivery({ status: "PENDING" })); + + expect(items.map((i) => i.id)).toEqual(["received", "routing"]); + const line = items[1]; + expect(line.type).toBe("line"); + expect(line).toMatchObject({ from: createdAt, to: null, state: "inprogress" }); + }); + + it("PROCESSING is also in-flight (no terminal node)", () => { + const items = buildDeliveryTimelineItems(baseDelivery({ status: "PROCESSING" })); + + expect(items.map((i) => i.id)).toEqual(["received", "routing"]); + expect(items[1]).toMatchObject({ to: null, state: "inprogress" }); + }); + + it("SUCCEEDED renders a Delivered terminal node with the run/session target", () => { + const items = buildDeliveryTimelineItems( + baseDelivery({ + status: "SUCCEEDED", + processedAt, + run: { friendlyId: "run_2f4b" }, + session: { friendlyId: "sess_xyz", externalId: "cust_1" }, + }) + ); + + expect(items.map((i) => i.id)).toEqual(["received", "routing", "delivered"]); + expect(items[1]).toMatchObject({ from: createdAt, to: processedAt, state: "complete" }); + const terminal = items[2]; + expect(terminal).toMatchObject({ + type: "event", + title: "Delivered", + state: "complete", + date: processedAt, + target: { + run: { friendlyId: "run_2f4b" }, + session: { friendlyId: "sess_xyz", externalId: "cust_1" }, + }, + }); + }); + + it("FAILED renders a Failed terminal node carrying the error message", () => { + const items = buildDeliveryTimelineItems( + baseDelivery({ status: "FAILED", processedAt, errorMessage: "queue limit exceeded" }) + ); + + expect(items.map((i) => i.id)).toEqual(["received", "routing", "failed"]); + expect(items[1]).toMatchObject({ state: "error" }); + expect(items[2]).toMatchObject({ + title: "Failed", + state: "error", + note: "queue limit exceeded", + }); + }); + + it("FILTERED renders a dimmed Filtered node with the reason and no run target", () => { + const items = buildDeliveryTimelineItems( + baseDelivery({ + status: "FILTERED", + processedAt, + filterReason: "action != opened", + }) + ); + + expect(items.map((i) => i.id)).toEqual(["received", "routing", "filtered"]); + expect(items[1]).toMatchObject({ state: "delayed", variant: "light" }); + const terminal = items[2]; + expect(terminal).toMatchObject({ + type: "event", + title: "Filtered", + state: "delayed", + note: "action != opened", + }); + expect((terminal as { target?: unknown }).target).toBeUndefined(); + }); + + it("Received is always the first node and marked complete", () => { + for (const status of ["PENDING", "PROCESSING", "SUCCEEDED", "FAILED", "FILTERED"] as const) { + const items = buildDeliveryTimelineItems(baseDelivery({ status, processedAt })); + expect(items[0]).toMatchObject({ id: "received", title: "Received", state: "complete" }); + } + }); +}); diff --git a/apps/webapp/test/readBodyWithCap.test.ts b/apps/webapp/test/readBodyWithCap.test.ts new file mode 100644 index 00000000000..2ed7be0c402 --- /dev/null +++ b/apps/webapp/test/readBodyWithCap.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { readBodyWithCap } from "~/utils/readBodyWithCap.server"; + +function streamingRequest(chunks: Uint8Array[]): Request { + const body = new ReadableStream({ + pull(controller) { + const next = chunks.shift(); + if (next) controller.enqueue(next); + else controller.close(); + }, + }); + return new Request("https://example.com/ingest", { + method: "POST", + body, + // @ts-expect-error duplex isn't in the lib types yet but Node requires it for a stream body + duplex: "half", + }); +} + +const chunk = (n: number) => new Uint8Array(n).fill(122); + +describe("readBodyWithCap", () => { + it("returns the full body when under the cap", async () => { + const bytes = await readBodyWithCap(streamingRequest([chunk(100), chunk(100)]), 1024); + expect(bytes).not.toBeNull(); + expect(bytes!.byteLength).toBe(200); + }); + + it("returns null when the streamed body exceeds the cap (no full buffering)", async () => { + const bytes = await readBodyWithCap( + streamingRequest([chunk(1024), chunk(1024), chunk(1024)]), + 2048 + ); + expect(bytes).toBeNull(); + }); + + it("treats an empty body as zero bytes", async () => { + const bytes = await readBodyWithCap( + new Request("https://example.com/ingest", { method: "POST" }), + 1024 + ); + expect(bytes).not.toBeNull(); + expect(bytes!.byteLength).toBe(0); + }); +}); diff --git a/docker/Dockerfile.postgres b/docker/Dockerfile.postgres deleted file mode 100644 index c71f98cecb3..00000000000 --- a/docker/Dockerfile.postgres +++ /dev/null @@ -1,5 +0,0 @@ -FROM postgres:14 - -RUN apt-get update \ - && apt-get install -y --no-install-recommends postgresql-14-partman \ - && rm -rf /var/lib/apt/lists/* diff --git a/docker/dev-compose.yml b/docker/dev-compose.yml index 27f0ba18595..4873603aa7a 100644 --- a/docker/dev-compose.yml +++ b/docker/dev-compose.yml @@ -12,9 +12,7 @@ networks: services: db: container_name: db-dev - build: - context: . - dockerfile: Dockerfile.postgres + image: postgres:14 restart: always volumes: - database-data:/var/lib/postgresql/data/ @@ -31,8 +29,6 @@ services: - listen_addresses=* - -c - wal_level=logical - - -c - - shared_preload_libraries=pg_partman_bgw electric: container_name: electric-dev diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index bc682658d21..9e7d5d9f365 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,9 +25,7 @@ networks: services: database: container_name: ${CONTAINER_PREFIX:-}database - build: - context: . - dockerfile: Dockerfile.postgres + image: postgres:14 restart: always volumes: - ${DB_VOLUME:-database-data}:/var/lib/postgresql/data/ @@ -44,8 +42,6 @@ services: - listen_addresses=* - -c - wal_level=logical - - -c - - shared_preload_libraries=pg_partman_bgw # The webapp opens ~50 pooled connections per instance and Electric another # ~40, so the default 100 is exhausted by one webapp + Electric alone. Raise # it so multiple instances / load tests have headroom. @@ -94,9 +90,7 @@ services: database-replica: container_name: ${CONTAINER_PREFIX:-}database-replica profiles: ["replica"] - build: - context: . - dockerfile: Dockerfile.postgres + image: postgres:14 restart: always depends_on: - database diff --git a/docs/ai-chat/backend.mdx b/docs/ai-chat/backend.mdx index c055570ef16..4aa8fdb14d4 100644 --- a/docs/ai-chat/backend.mdx +++ b/docs/ai-chat/backend.mdx @@ -470,6 +470,37 @@ Custom actions let the frontend send structured commands (undo, rollback, edit, See [Actions](/ai-chat/actions). +### Webhook events and channels + +Two `chat.agent()` options wire an agent to verified inbound webhooks. `events` claims [`chat.event(...)`](/webhooks/session-routing) descriptors: each verified delivery is routed to this agent's session and arrives at `onAction` as an action (not a turn), so [session routing](/webhooks/session-routing) decides which conversation it lands on. `channels` claims channel connectors that turn an external chat surface into a frontend for the agent: an inbound message runs as a turn through `run()` and the reply is posted back. `slack()` ships in `@trigger.dev/slack`, and `chat.channels.custom(...)` builds a connector for any source without a preset. + +```ts +import { webhooks } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; +import { slack } from "@trigger.dev/slack"; + +export const orderEvents = chat.event({ + id: "order-events", + source: webhooks.stripe(), + key: "{body.data.object.customer}", + type: "order.event", +}); + +export const myChat = chat.agent({ + id: "my-chat", + events: [orderEvents], + channels: [slack({ id: "support-slack", token: process.env.SLACK_BOT_TOKEN! })], + onAction: async ({ action }) => { + // A verified order-events delivery arrives here as an action. + }, + run: async (payload) => { + // Inbound Slack messages run here as normal turns. + }, +}); +``` + +See [session routing](/webhooks/session-routing) and [channels](/webhooks/channels). For the interactive approvals layer, where a turn pauses on a human decision (buttons in the thread) and resumes on the click, see [human-in-the-loop](/webhooks/human-in-the-loop). + ### Chat history Imperative API for reading and modifying the accumulated message history. Works from any hook (`onAction`, `onTurnStart`, `onBeforeTurnComplete`, `onTurnComplete`, `hydrateMessages`) or from `run()` and AI SDK tools. diff --git a/docs/ai-chat/reference.mdx b/docs/ai-chat/reference.mdx index da08f9a0473..fa5f6201691 100644 --- a/docs/ai-chat/reference.mdx +++ b/docs/ai-chat/reference.mdx @@ -47,6 +47,8 @@ Options for `chat.agent()`. | `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise` | — | Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) | | `actionSchema` | `TaskSchema` | — | Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/actions) | | `onAction` | `(event: ActionEvent) => Promise \| unknown` | — | Handle custom actions. Actions are not turns — only `hydrateMessages` + `onAction` fire. Return a `StreamTextResult` (or `string` / `UIMessage`) for a model response; return `void` for side-effect-only. See [Actions](/ai-chat/actions) | +| `events` | `ChatEvent[]` | — | Webhook event descriptors (from `chat.event()`) whose verified deliveries are routed to this agent as actions and handled in `onAction`. See [session routing](/webhooks/session-routing). | +| `channels` | `ChannelConnector[]` | — | Channel connectors (for example `slack()`) that turn an external chat surface into a frontend for the agent: inbound messages run as turns and the reply posts back. See [channels](/webhooks/channels). | | `onTurnStart` | `(event: TurnStartEvent) => Promise \| void` | — | Fires every turn before `run()` | | `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise \| void` | — | Fires after response but before stream closes. Includes `writer`. | | `onTurnComplete` | `(event: TurnCompleteEvent) => Promise \| void` | — | Fires after each turn completes (stream closed) | @@ -501,6 +503,8 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`. | Method | Description | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `chat.agent(options)` | Create a chat agent | +| `chat.event(options)` | Declare an inbound webhook event descriptor an agent claims via `chat.agent({ events })`. See [session routing](/webhooks/session-routing). | +| `chat.channels.custom(options)` | Create a generic chat-frontend channel over any verified webhook source (you supply the egress). The `slack()` preset ships in `@trigger.dev/slack`. See [channels](/webhooks/channels). | | `chat.createSession(payload, options)` | Create an async iterator for chat turns | | `chat.pipe(source, options?)` | Pipe a stream to the frontend (from anywhere inside a task) | | `chat.pipeAndCapture(source, options?)` | Pipe and capture the response; returns `{ message, status, error }` | @@ -530,6 +534,42 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`. | `chat.withUIMessage(config?)` | Returns a [ChatBuilder](/ai-chat/types#chatbuilder) with a fixed `UIMessage` subtype. See [Types](/ai-chat/types) | | `chat.withClientData({ schema })` | Returns a [ChatBuilder](/ai-chat/types#chatbuilder) with a fixed client data schema. See [Types](/ai-chat/types#typed-client-data-with-chatwithclientdata) | +## `chat.event` + +Declare an inbound webhook event that an agent claims via [`events`](#chatagentoptions) on `chat.agent()`. It is a descriptor only, with no handler: it names a [source](/webhooks/sources) to verify, a `key` template that resolves each delivery to a durable [session](/ai-chat/sessions), and an optional `type` label (defaults to the descriptor `id`). Verified deliveries are routed to that session and arrive at `onAction` as a `{ type, event, source, headers, deliveryId }` envelope, not as a chat turn. See [session routing](/webhooks/session-routing). + +```ts +import { webhooks } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; + +export const orderEvents = chat.event({ + id: "order-events", + source: webhooks.stripe(), + key: "{body.data.object.customer}", + type: "order.event", +}); +``` + +## `chat.channels.custom` + +Create a generic chat-frontend channel over any verified [source](/webhooks/sources), claimed via [`channels`](#chatagentoptions) on `chat.agent()`. You supply the session `key`, the `inbound` map from event to turn message, and your own `send` egress that posts the reply back, so the whole round-trip is under your control. Inbound messages run as normal turns and the reply is posted back. The `slack()` preset ships in `@trigger.dev/slack` and wires the egress for you. See [channels](/webhooks/channels), and the interactive approvals layer at [human-in-the-loop](/webhooks/human-in-the-loop). + +```ts +import { webhooks } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; + +export const mySurface = chat.channels.custom({ + id: "my-surface", + source: webhooks.custom({ /* verifier config */ }), + key: "{body.conversationId}", + inbound: (event) => event.text, + send: async (message, ctx) => { + const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef); + return { ref }; + }, +}); +``` + ## `chat.withUIMessage` Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed `UIMessage` subtype. Chain `.withClientData()`, hook methods, and `.agent()`. diff --git a/docs/docs.json b/docs/docs.json index ad5ac311e67..f20ccb81b2a 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -146,6 +146,19 @@ } ] }, + { + "group": "Webhooks", + "pages": [ + "webhooks/overview", + "webhooks/sources", + "webhooks/connect", + "webhooks/deliveries", + "webhooks/filters", + "webhooks/session-routing", + "webhooks/channels", + "webhooks/human-in-the-loop" + ] + }, { "group": "Configuration", "pages": [ diff --git a/docs/webhooks/channels.mdx b/docs/webhooks/channels.mdx new file mode 100644 index 00000000000..eb1d36ff499 --- /dev/null +++ b/docs/webhooks/channels.mdx @@ -0,0 +1,133 @@ +--- +title: "Channels (chat frontends)" +description: "Point Slack (or any chat surface) at an agent: messages become turns and replies post back." +sidebarTitle: "Channels" +--- + +A [session route](/webhooks/session-routing) delivers a verified event to an agent as an [action](/ai-chat/actions): the agent reacts, and the response is a side effect. A **channel** is the other half: the webhook IS the chat surface. Inbound messages become **turns** (the normal `run()` loop), and the agent's reply is posted **back** to the surface. A Slack thread becomes a real conversation with the agent, exactly like the browser chat, just a different frontend. + +List channels on a [`chat.agent`](/ai-chat/overview) alongside (or instead of) `events`: + +```ts +import { chat } from "@trigger.dev/sdk/ai"; +import { slack } from "@trigger.dev/slack"; + +export const supportAgent = chat.agent({ + id: "support-agent", + channels: [slack({ id: "support-slack", token: process.env.SLACK_BOT_TOKEN! })], + run: async ({ messages }) => streamText({ model: anthropic("claude-sonnet-4-5"), messages }), +}); +``` + +The `run()` loop is unchanged: the agent does not know or care that it is talking to Slack. One verified Slack message in a thread is routed to a durable [session](/ai-chat/sessions) keyed to that thread, run as a turn, and the reply is posted into the thread. + +## Slack + +`slack()` (from `@trigger.dev/slack`) is a channel connector: it verifies inbound Slack events, maps a message to the turn, and posts the reply back with `chat.postMessage` / `chat.update`. + + + + Create an app at [api.slack.com/apps](https://api.slack.com/apps). Add the `chat:write` bot scope and install it to your workspace to get a bot token (`xoxb-...`). + + + Deploying registers a hosted [endpoint](/webhooks/connect) for the channel. Set its signing secret to your Slack app's **Signing Secret**, and pass the bot token as `token`. + + + In the app's **Event Subscriptions**, set the request URL to the endpoint's webhook URL. Slack sends a one-time `url_verification` handshake, which the endpoint answers automatically. Subscribe the bot to `message.channels`, then invite the bot to the channel (`/invite @yourapp`). + + + +By default `slack()` keys one session per thread, strips the leading bot mention from the message, posts an "on it..." placeholder while the agent works, and edits it to the answer. Override any of that: + +```ts +slack({ + id: "support-slack", + token: process.env.SLACK_BOT_TOKEN!, + // ignore anything but questions (composed with the built-in self-message guard) + filter: "event.event.text contains '?'", + inbound: (e) => e.event?.text ?? "", + outbound: (reply) => ({ text: reply.text }), + ack: (e) => ({ text: "thinking..." }), // pass `null` to post only the final answer +}); +``` + + + `slack()` always drops the bot's own messages (and their edits) before they reach the agent, so the + agent never replies to itself. A multi-workspace app can pass a `token` resolver keyed on the event's + team instead of a single string. + + +### Summoning with a mention + +By default `slack()` starts (or resumes) a session for every non-bot message in a subscribed channel. To make the agent respond only when it is @mentioned, pass `startOn` with the `mentions` helper. The first mention in a thread starts the session, and the agent then follows the rest of the thread without needing to be mentioned again. + +```ts +import { slack, mentions } from "@trigger.dev/slack"; + +slack({ + id: "support-slack", + token: process.env.SLACK_BOT_TOKEN!, + startOn: mentions("U012BOT"), // your bot's user id (pass several for multiple bots) +}); +``` + +### Reacting to messages + +`slack()` can add an emoji reaction to the triggering message to signal progress. Set `reactions` with any of `working`, `done`, and `error`: the connector adds `working` when the turn starts, swaps it to `done` when the turn finishes, and reacts with `error` if it fails. This needs the `reactions:write` scope. + +```ts +slack({ + id: "support-slack", + token: process.env.SLACK_BOT_TOKEN!, + reactions: { working: "eyes", done: "white_check_mark", error: "warning" }, +}); +``` + +### Options + +| Option | Type | Description | +| --- | --- | --- | +| `id` | `string` | Connector id, unique per agent. | +| `token` | `string` or resolver | Bot token (`xoxb-...`), or a function of the event's team for multi-workspace apps. | +| `key` | `string` | Session [key](/webhooks/session-routing) template. Defaults to one session per thread. | +| `filter` | `string` | Extra [filter](/webhooks/filters), composed with the built-in self-message guard. | +| `startOn` | `string` | Only start a session when the event matches (see `mentions`). Existing sessions always resume. | +| `ack` | message, `null`, or function | Placeholder posted while the agent works. Pass `null` to post only the final answer. | +| `reactions` | `{ working?, done?, error? }` | Lifecycle emoji reactions on the triggering message. | +| `inbound` / `outbound` | functions | Map the Slack event to the turn, and the reply to a Slack message. | +| `delivery` | `"final"` or `"stream"` | `"final"` (default) posts a placeholder and edits it to the answer. `"stream"` edits live as the reply streams. | +| `apiBaseUrl` | `string` | Override the Slack Web API base, for testing against a mock. | + +## Approvals and interactive controls + +An agent on a channel can pause a turn to get a human decision, approving a refund or confirming a deletion, and resume once someone clicks a button in the thread. `slack()` renders Approve / Deny buttons for you and collapses them to the decision once clicked. See [human-in-the-loop](/webhooks/human-in-the-loop). + +## Any surface: `chat.channels.custom` + +For a surface without a preset, `chat.channels.custom` is the generic connector. You supply the [source](/webhooks/sources) to verify, the session `key`, the `inbound` map, and the egress `send`: + +```ts +import { chat } from "@trigger.dev/sdk/ai"; +import { webhooks } from "@trigger.dev/sdk"; + +const mySurface = chat.channels.custom({ + id: "my-surface", + source: webhooks.custom({ /* verifier config */ }), + key: "{body.conversationId}", + inbound: (e) => e.text, + outbound: (reply) => (reply.text ? { text: reply.text } : null), // null posts nothing + send: async (message, ctx) => { + const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef); + return { ref }; // an existing ref means edit-in-place on the next turn + }, +}); +``` + +`send` is called to post the reply. `ctx.previousRef` is the ref you returned last time, so streaming or a follow-up edits the same message instead of posting a new one. Return `null` from `outbound` to stay silent (a tool-only turn, say). + +## Channels vs events + +Both are inbound surfaces on a `chat.agent`, and an agent can list both: + +- [`events`](/webhooks/session-routing) (`chat.event`): the webhook is a signal. Delivered to `onAction`; the agent acts, no reply is sent back. +- `channels` (`slack`, `chat.channels.custom`): the webhook is a chat frontend. Delivered as a turn to `run()`; the reply is posted back. diff --git a/docs/webhooks/connect.mdx b/docs/webhooks/connect.mdx new file mode 100644 index 00000000000..0f6430e117a --- /dev/null +++ b/docs/webhooks/connect.mdx @@ -0,0 +1,35 @@ +--- +title: "Connecting a provider" +description: "Point a provider at the webhook URL and set the signing secret." +sidebarTitle: "Connecting a provider" +--- + +When you deploy (or run `dev`), each webhook task gets an **endpoint** with a unique, unguessable webhook URL. Open the webhook in the dashboard, go to **Endpoints**, and open the endpoint to find its **Connect** panel. + + + + Copy it from the endpoint's Connect panel. On Trigger.dev Cloud it looks like + `https://webhooks.trigger.dev/webhooks/v1/ingest/`. A self-hosted instance serves it from that + instance's own base URL. This is what you give the provider as its webhook destination. + + + A webhook can't accept deliveries until its signing secret is set. Until then every request is + rejected. There are two flows, and the Connect panel shows the right one for the provider: + + - **The provider generates the secret** (Stripe, Svix): copy it from the provider and paste it + into **Set secret**. + - **You choose the secret** (GitHub, or a service you control): click **Generate secret** and + Trigger.dev mints a strong secret and shows it once. Paste that into the provider's webhook config. + + + Add the webhook URL as the destination in your provider's dashboard. The Connect panel + shows the exact signature scheme (header, algorithm, signing string) the provider should use. + + + + + The signing secret is stored encrypted and is never shown again after it's set. To rotate it, + use **Rotate secret** (or **Regenerate**) and update the provider with the new value. + + +Once a provider is sending events, watch them arrive on the [Deliveries](/webhooks/deliveries) page, which also explains what an [endpoint](/webhooks/deliveries#endpoints) is. diff --git a/docs/webhooks/deliveries.mdx b/docs/webhooks/deliveries.mdx new file mode 100644 index 00000000000..95fcbed6519 --- /dev/null +++ b/docs/webhooks/deliveries.mdx @@ -0,0 +1,48 @@ +--- +title: "Deliveries and endpoints" +description: "Observe inbound webhook requests, the runs they trigger, and their payloads in the dashboard." +sidebarTitle: "Deliveries & endpoints" +--- + +The dashboard surfaces two concepts under the **Webhooks** section. + +## Deliveries + +A delivery is a single inbound request that passed verification. The **Deliveries** page lists every +delivery across all your webhooks (much like the Runs page), and you can filter by webhook, status, +delivery id, or run id. + +Open a delivery to see: + +- Its **status** and the **run** it triggered (linked). +- The verified **event payload** and the inbound **request headers**, on separate tabs. +- The external delivery id, idempotency key, and timestamps. + + + Duplicate deliveries are deduplicated automatically. The idempotency key is the provider's event id + (e.g. the Stripe event id, or GitHub's `X-GitHub-Delivery`), so a provider retry of the same event + resolves to the original delivery and won't trigger a second run. + + +## Endpoints + +An endpoint is the connection instance for a webhook: its webhook URL, signing-secret state, +verification scheme, and delivery history. Each webhook's **Endpoints** tab lists its endpoints (a +declared webhook has one), and opening an endpoint shows its [Connect panel](/webhooks/connect) and +its scoped deliveries. + +## What happens to a request + + + + The signature, timestamp, and idempotency key are checked. A failure returns `400` and records + nothing. + + + A verified request becomes a delivery, with its parsed event and headers stored. + + + The delivery is routed to your webhook task, which runs and calls `onEvent`. The delivery's status + reflects that run's outcome. + + diff --git a/docs/webhooks/filters.mdx b/docs/webhooks/filters.mdx new file mode 100644 index 00000000000..4ab7f7f8483 --- /dev/null +++ b/docs/webhooks/filters.mdx @@ -0,0 +1,99 @@ +--- +title: "Filtering deliveries" +description: "Gate which verified webhook deliveries run, with a type-safe filter checked against the event." +sidebarTitle: "Filters" +--- + +By default every verified delivery runs your `onEvent` (or routes to a [session](/webhooks/session-routing)). A **filter** is a server-side predicate that decides whether a delivery is routed at all. A delivery that does not match is still received and recorded, it just does not run anything. + +Filtering happens at the endpoint, before any run is triggered, so a filtered-out event costs you nothing. + +## Adding a filter + +Pass a `filter` string to `webhook()`. It is a small expression checked, at build time, against the event shape from your [source](/webhooks/sources#typing-the-event): + +```ts +import { webhook, webhooks } from "@trigger.dev/sdk"; + +export const onOrder = webhook({ + id: "orders", + source: webhooks.stripe(), + // only route succeeded payment intents over $100 + filter: "event.type == 'payment_intent.succeeded' && event.data.object.amount >= 10000", + onEvent: async ({ event }) => { + // only runs for deliveries that matched + }, +}); +``` + +The filter is type-safe: referencing a field that does not exist, or comparing it to the wrong kind of literal, is a compile error, not a runtime surprise. + +## What a non-match does + +A delivery that does not match is **not dropped**. It still returns `200` to the provider and is recorded as a [delivery](/webhooks/deliveries) with the status `FILTERED` and a reason naming the clause that failed (and the value it saw). It just never triggers a run. This keeps a filtered delivery auditable: you can see in the dashboard that it arrived and why it was not routed. + + + If a filter throws while evaluating (for example, a malformed event), the delivery is routed rather + than dropped. Filters fail open so a filter bug never silently swallows real events. + + +## The expression language + +A filter is one or more `path operator value` clauses combined with `&&` and `||` (use parentheses to group). + +### Paths + +A path reads from one of three namespaces: + +- `event.*`: the verified, parsed request body, for example `event.data.object.amount`. +- `header.*`: an inbound request header, matched case-insensitively, for example `header.x-github-event`. +- `webhook.*`: endpoint metadata (`webhook.source`, `webhook.id`, `webhook.deliveryId`, and for per-tenant endpoints `webhook.externalRef` / `webhook.tenantId`). + +The [session routing](/webhooks/session-routing) key template addresses this same parsed body, but spells it `{body.*}` rather than `event.*`. + +### Operators + +| Operator | Meaning | +| --- | --- | +| `==` `!=` | equality | +| `>` `<` `>=` `<=` | numeric comparison | +| `in` `not in` | membership in a list, for example `event.type in ['a','b']` | +| `startsWith` `endsWith` `contains` | string matching | + +Values are strings in single quotes (`'created'`), numbers (`10000`), booleans (`true`), or a list for `in` / `not in`. + +### Comparing two fields + +The right-hand side can be another path instead of a literal, so you can compare two fields of the same event: + +```ts +filter: "event.billing.country == event.shipping.country"; +``` + +### Matching inside a list + +`any` and `all` quantify over an array, testing a sub-path on each element: + +```ts +// route only if at least one line item has a positive quantity +filter: "event.items any ( quantity > 0 )"; +``` + +### Spacing + +The type checker reads the filter as a token stream, so a couple of spots are strict about spacing: keep `in` / `not in` lists unspaced (`['a','b']`, not `[ 'a', 'b' ]`) and put spaces around the quantifier parentheses (`any ( ... )`). + +To match only certain event types, write a clause against the field that carries the type: `event.type` for Stripe / Svix / Square / Discord, or the `x-github-event` header for GitHub (the filter DSL can read a `header.` namespace too): + +```ts +export const onGithub = webhook({ + id: "github", + source: webhooks.github(), + filter: "header.x-github-event in ['issues','pull_request']", + onEvent: async ({ event }) => {}, +}); +``` + +## Filtering a session route + +A `filter` works the same way on [`chat.event`](/webhooks/session-routing): a non-matching delivery is recorded `FILTERED` and never reaches the session. diff --git a/docs/webhooks/human-in-the-loop.mdx b/docs/webhooks/human-in-the-loop.mdx new file mode 100644 index 00000000000..1fc52cbce61 --- /dev/null +++ b/docs/webhooks/human-in-the-loop.mdx @@ -0,0 +1,143 @@ +--- +title: "Human-in-the-loop" +sidebarTitle: "Human-in-the-loop" +description: "Pause an agent mid-turn to get a human decision from the channel (Slack approve/deny), then resume with the answer." +--- + +**An agent on a [channel](/webhooks/channels) can pause a turn to get a human decision, then resume once someone answers in the thread.** It posts controls into the conversation and picks up where it left off once someone clicks, with the decision merged into the turn. This is the channel counterpart of browser [human-in-the-loop](/ai-chat/patterns/human-in-the-loop): the same no-`execute` tool, but the controls live in Slack (or your own surface) instead of a React component. The [`slack()`](/webhooks/channels) connector ships Approve / Deny buttons out of the box. + +## How it works + +The building block is a tool with no `execute` function. When the model calls it, the turn completes with the tool call still pending instead of resolving it. Over a channel the framework then takes over the round-trip: + +```mermaid +sequenceDiagram + participant U as User + participant S as Slack + participant A as Agent run + U->>S: Message in a thread + S->>A: Verified delivery starts a turn + A->>A: Model calls requestApproval (no execute) + A->>S: renderInteraction posts Approve / Deny + Note over A: Turn completes, run suspends (no compute while waiting) + U->>S: Clicks a button + S->>A: Signed block_actions callback to the same webhook URL + A->>A: onInteraction resolves the pending tool + A->>S: finalizeInteraction collapses the buttons + A->>A: Run resumes, model continues + A->>S: Reply posts back to the thread +``` + +Because it is a no-`execute` pause, the run suspends while it waits rather than holding compute. A human can take minutes or days to decide without burning compute or hitting [`maxDuration`](/runs/max-duration). The mechanics are the same as the browser case, covered in [human-in-the-loop](/ai-chat/patterns/human-in-the-loop#duration-and-cost-while-paused). + +## Slack approvals + +Out of the box, `slack()` posts Approve / Deny buttons for any pending no-`execute` tool. A click resolves to `{ approved: boolean }` and the buttons collapse to the decision. You define the tool and list the channel on the agent: + +```ts trigger/support-agent.ts +import { chat } from "@trigger.dev/sdk/ai"; +import { slack } from "@trigger.dev/slack"; +import { streamText, tool } from "ai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { z } from "zod"; + +// No execute: calling this pauses the turn for a human decision. +const requestApproval = tool({ + description: + "Request human approval before a sensitive or irreversible action. " + + "Call this and stop; you will receive { approved: boolean }, then proceed or decline.", + inputSchema: z.object({ + action: z.string().describe("A short description of the action needing approval"), + }), +}); + +export const supportAgent = chat.agent({ + id: "support-agent", + channels: [slack({ id: "support-slack", token: process.env.SLACK_BOT_TOKEN! })], + tools: { requestApproval }, + run: async ({ messages, tools }) => + streamText({ + model: anthropic("claude-sonnet-4-5"), + messages, + tools, + system: + "Before any sensitive or irreversible action (refunds, cancellations, deletions), " + + "call requestApproval and wait. If approved, confirm it is done; if denied, decline.", + }), +}); +``` + +Button clicks arrive on a different Slack API than messages, so there is one extra setup step beyond [connecting the channel](/webhooks/channels): + + + + In the Slack app's **Interactivity & Shortcuts**, enable interactivity and set the Request URL to the **same** webhook URL you used for events. Slack posts button clicks there as a signed `block_actions` callback, which the endpoint verifies and routes to the paused run. + + + Posting and editing the controls uses the `chat:write` scope you already added for replies. No extra scope is needed to collapse the buttons after a decision. + + + +When the model calls `requestApproval`, the bot posts "Approval needed" with the action and Approve / Deny buttons. Clicking **Approve** resolves the tool to `{ approved: true }`, the buttons collapse to "Approved by @you", and the agent continues and posts the outcome. **Deny** resolves `{ approved: false }`. + +## Customizing the controls + +For [`chat.channels.custom`](/webhooks/channels), or to override the Slack defaults, three hooks own the interaction round-trip: + + + Map the pending tool call(s) to the controls you post. `pending` is a list of `{ toolCallId, toolName, input }`. Return `null` to skip posting controls. + + + + Map a verified callback event to a resolution. The `output` is stitched onto the pending tool call matched by `toolCallId` and the run resumes. Return `null` to treat the event as a normal message instead (a new turn). + + + + Collapse the controls after a decision so they cannot be clicked again. Best effort: a failure here is logged and the run still resumes. + + +Slack encodes the decision in each button's `value` as `${toolCallId}::approve|deny` so `onInteraction` can resolve the exact call, and `finalizeInteraction` posts to the interaction's `response_url` with `replace_original` to swap the buttons for the outcome. On a custom surface you choose the encoding and how you edit the controls away. + +```ts +chat.channels.custom({ + id: "my-surface", + source: webhooks.custom({ /* verifier config */ }), + key: "{body.conversationId}", + inbound: (e) => e.text, + send: async (message, ctx) => ({ ref: await postToMySurface(ctx.event, message) }), + renderInteraction: (pending) => ({ + text: `Approve: ${pending[0].input.action}?`, + buttons: pending.map((c) => [`${c.toolCallId}:yes`, `${c.toolCallId}:no`]), + }), + onInteraction: (e) => { + const [toolCallId, choice] = e.buttonValue.split(":"); + return { toolCallId, output: { approved: choice === "yes" } }; + }, + finalizeInteraction: async (e) => { + await editMySurface(e.messageRef, "Decision recorded"); + }, +}); +``` + +The resolved `output` is whatever your tool expects to receive. The Slack default resolves `{ approved: boolean }`; if your tool needs a richer answer, supply your own `onInteraction` that returns the shape your tool reads. + +## The reply after a decision + +When the turn resumes, the channel reply shows the agent's answer, not the text it produced before pausing. The framework posts the assistant text generated after the tool call, so a preamble like "I'll need approval first" is not concatenated onto the final "done" message. The approval prompt itself already lives in the collapsed controls. + +## Next steps + + + + Point Slack or any surface at an agent so messages become turns. + + + The same no-execute pause, rendered in a React frontend. + + + The durable per-conversation state a channel turn runs on. + + + Gate which verified deliveries reach the agent. + + diff --git a/docs/webhooks/overview.mdx b/docs/webhooks/overview.mdx new file mode 100644 index 00000000000..73dc3b35447 --- /dev/null +++ b/docs/webhooks/overview.mdx @@ -0,0 +1,96 @@ +--- +title: "Webhooks overview" +description: "Receive and verify webhooks from external providers as a task, with a hosted webhook URL." +sidebarTitle: "Overview" +--- + +A webhook is a task that runs when an external provider (Stripe, GitHub, Svix, your own service, …) sends an HTTP request. Trigger.dev gives each webhook a hosted webhook URL, verifies the incoming request's signature, and routes the verified event to your task's `onEvent` handler. + +You don't host an endpoint yourself, and you don't write verification code: you declare which provider the webhook is from, point the provider at the webhook URL, and set the signing secret. + +## Defining a webhook task + +A webhook is created with `webhook()`. It takes an `id`, a `source` (which provider, and how to verify it), and an `onEvent` handler: + +```ts +import { webhook, webhooks } from "@trigger.dev/sdk"; + +export const onStripeEvent = webhook({ + id: "stripe-events", + source: webhooks.stripe(), + onEvent: async ({ event, headers, ctx }) => { + // `event` is the verified, parsed body + console.log("Received", event.type, event.id); + + // `headers` is a standard Web Headers object + console.log(headers.get("stripe-signature")); + + // `ctx` is the usual run context + console.log(ctx.run.id); + }, +}); +``` + +`onEvent` receives: + +- **`event`**: the verified request body, parsed from JSON and typed by the source (see [Typing the event](/webhooks/sources#typing-the-event)). +- **`headers`**: the inbound request headers as a Web [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object (case-insensitive `.get()` / `.has()`). +- **`ctx`**: the run context, the same one regular tasks receive. + + + A webhook is a first-class task kind. It runs on a real run (with retries, logs, and everything else + tasks get), and shows up in the dashboard alongside your other tasks. + + +## How it works + + + + Define a `webhook()` with a `source`. The source is a provider preset (like `webhooks.stripe()`) + or a `webhooks.custom()` config. See [Sources and verification](/webhooks/sources). + + + Deploying the webhook creates an endpoint with a hosted webhook URL. Set its signing secret and + point your provider at the URL. See [Connecting a provider](/webhooks/connect). + + + Each inbound request is verified, recorded as a delivery, and routed to a run that calls your + `onEvent`. See [Deliveries and endpoints](/webhooks/deliveries). + + + +## Beyond fan-out + +A few things build on the basic model: + +- **[Filters](/webhooks/filters)** gate which deliveries run. A non-matching delivery is recorded but never triggers a run. +- **[Session routing](/webhooks/session-routing)** sends deliveries that share a key to one durable session (per customer, installation, or issue) instead of a fresh run each time. +- **[Channels](/webhooks/channels)** turn a webhook into a chat frontend: inbound messages become agent turns, and the agent's reply posts back to the surface. +- **[Human-in-the-loop](/webhooks/human-in-the-loop)** adds approvals and interactive controls over a channel, like Slack approve and deny buttons. + + + + Provider presets, custom verification, and typing the event. + + + The webhook URL and signing secret. + + + Observe inbound requests in the dashboard. + + + Route only the deliveries you care about. + + + Route deliveries to a durable per-key session. + + + Turn a webhook into a chat frontend for an agent. + + + Approvals and interactive controls over a channel. + + + The other declarative task trigger. + + diff --git a/docs/webhooks/session-routing.mdx b/docs/webhooks/session-routing.mdx new file mode 100644 index 00000000000..575d8914c03 --- /dev/null +++ b/docs/webhooks/session-routing.mdx @@ -0,0 +1,96 @@ +--- +title: "Routing to a session" +description: "Route verified webhook deliveries to a durable per-key session instead of a fresh run." +sidebarTitle: "Session routing" +--- + +A plain [`webhook()`](/webhooks/overview) runs a fresh, stateless run for every delivery. Sometimes you want the opposite: deliveries that share a key (a customer, an installation, an issue) should land on **one durable [session](/ai-chat/sessions)** and be handled in order, with state carried across them. That is what session routing does. + +Instead of a handler, you declare a `chat.event` and list it on an agent. The verified delivery is routed to a find-or-created session and arrives as an [action](/ai-chat/actions). + +## Declaring a chat event + +`chat.event` describes the routing only: the [source](/webhooks/sources) to verify, a `key` that identifies the session, and a `type` label for the delivered action. It has no handler. + +```ts +import { webhooks } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; + +export const orderEvents = chat.event({ + id: "order-events", + source: webhooks.stripe(), + // one session per customer + key: "{body.data.object.customer}", + type: "order.event", +}); +``` + +### The key + +The `key` is what makes routing durable: two deliveries that resolve to the same key reach the same session; a different key gets its own. It is a template string, evaluated server-side at ingest, whose `{...}` placeholders read three namespaces: + +- `{body.*}`: the parsed body (a bare `{customer}` defaults to the body namespace). +- `{webhook.*}`: endpoint metadata (`externalRef`, `tenantId`, `id`, `source`, `deliveryId`). +- `{header.name}`: an inbound header. + +[Filter](/webhooks/filters) expressions read this same parsed body, but spell it `event.*` rather than `{body.*}`. + +Compose several placeholders into one key: `"{webhook.externalRef}-{body.issue.id}"`. Each placeholder is checked against the event type at build time, so a bad field is a red squiggle on the `key` line, not a runtime surprise. + +### The `type` label + +`type` is a name you choose for the delivered action. It flows straight through to `action.type` on the envelope, so your handler can tell webhook actions apart from each other and from browser actions. It is optional and defaults to the descriptor `id`. + +## Handling deliveries on an agent + +List the descriptor on a [`chat.agent`](/ai-chat/overview) (or a session agent). The delivery arrives at `onAction`, not as a chat message: + +```ts +import { chat } from "@trigger.dev/sdk/ai"; +import { orderEvents } from "./order-events"; + +export const orderAgent = chat.agent({ + id: "order-agent", + events: [orderEvents], + onAction: async ({ action }) => { + switch (action.type) { + case "order.event": + // action.event is the verified Stripe event, fully typed + console.log("order for", action.event.data.object.customer); + break; + } + }, + run: async ({ messages, signal }) => { + /* ... */ + }, +}); +``` + +`action.type` is a closed union of the `type` labels of the events you listed, and each arm narrows `action.event` to that event's payload type. The full envelope is `{ type, event, source, headers, deliveryId }`. + +Because it is an [action](/ai-chat/actions), the handler is not a turn: it can mutate session state and, if you return a `streamText` result, produce a model response. Webhook actions bypass your `actionSchema` (their shape is fixed and already typed). + + + The descriptor never names the agent, and the agent references the descriptor, so there is no + circular dependency. Whichever agent lists it becomes the routing target. + + +## One endpoint per agent + +Listing a descriptor on an agent creates that agent's own endpoint, with its own [webhook URL and signing secret](/webhooks/connect). If two agents list the same descriptor, you get two endpoints, each routing to its own agent, and you point the provider at both URLs. + +An exported `chat.event` that no agent lists routes nothing. `trigger dev` warns about it (it is not an error, since declaring the descriptor before wiring the agent is a normal step), so wire it onto an agent once you are ready. + +## Filtering + +A [`filter`](/webhooks/filters) works here exactly as on a fan-out webhook: a non-matching delivery is recorded `FILTERED` and never reaches the session. + +```ts +export const orderEvents = chat.event({ + id: "order-events", + source: webhooks.stripe(), + key: "{body.data.object.customer}", + type: "order.event", + filter: "event.type == 'payment_intent.succeeded'", +}); +``` diff --git a/docs/webhooks/sources.mdx b/docs/webhooks/sources.mdx new file mode 100644 index 00000000000..7fa37067de1 --- /dev/null +++ b/docs/webhooks/sources.mdx @@ -0,0 +1,115 @@ +--- +title: "Sources and verification" +description: "Provider presets, custom verification config, and typing the webhook event." +sidebarTitle: "Sources & verification" +--- + +A webhook's `source` tells Trigger.dev which provider the request is from and how to verify it. Use a built-in preset, or `webhooks.custom()` for a provider without one. + +## Presets + +Built-in presets know the provider's signature scheme, so you don't configure anything: + + + +```ts Stripe +import { webhook, webhooks } from "@trigger.dev/sdk"; + +export const stripeWebhook = webhook({ + id: "stripe", + source: webhooks.stripe(), + onEvent: async ({ event }) => { + if (event.type === "payment_intent.succeeded") { + // ... + } + }, +}); +``` + +```ts GitHub +export const githubWebhook = webhook({ + id: "github", + source: webhooks.github(), + onEvent: async ({ event, headers }) => { + // GitHub puts the event type in a header + console.log(headers.get("x-github-event")); + }, +}); +``` + +```ts Svix +// Also covers Clerk, Resend, and other Svix-powered providers +export const svixWebhook = webhook({ + id: "svix", + source: webhooks.svix(), + onEvent: async ({ event }) => { + // ... + }, +}); +``` + + + +The available presets are `stripe()`, `github()`, `svix()`, `square()`, and `discord()`. + +## Custom providers + +For a provider without a preset, `webhooks.custom()` describes the scheme as data. For example, an HMAC-SHA256 signature over the raw body, in a custom header: + +```ts +export const customWebhook = webhook({ + id: "custom", + source: webhooks.custom<{ id: string; message: string }>({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-webhook-signature", + signingString: "raw", + idempotencyField: { from: "body", name: "id" }, + }), + onEvent: async ({ event }) => { + console.log(event.message); + }, +}); +``` + + + Reach for a preset first; drop to `custom()` for the long tail. A custom config can almost always + express a provider's scheme without any code. + + +## Typing the event + +Presets ship a sensible default event type, and they're generic, so you can plug in the provider's official type for full type-safety and autocomplete: + +```ts +import type Stripe from "stripe"; +import { webhooks } from "@trigger.dev/sdk"; + +// `event` is now the full, discriminated Stripe.Event union +webhooks.stripe(); +``` + +For `webhooks.custom()`, pass your own event type as `T`. + + + The event is typed but not re-validated against that type at runtime: once the signature is + verified, the body is trusted (the same model the official provider SDKs use). If you want runtime + validation, validate `event` inside `onEvent`. + + +## How verification works + +Every inbound request is verified before your task runs. Trigger.dev checks the signature, the +timestamp (for replay protection, where the provider supplies one), and derives the idempotency key +from the provider's event id. A request that fails verification gets a `400` and never creates a run. + +Presets handle this for you. Under the hood, every scheme is one of: + +- **`hmac`**: HMAC over the raw body or a templated signing string, signature in a header. The header can be a raw value, a prefixed one (like GitHub's `sha256=…`), or a structured one (like Stripe's `t=…,v1=…`). +- **`shared-secret`**: a static token compared in a header, bearer, basic auth, or the body. +- **`url-secret`**: a secret in the URL path or query string. +- **`asymmetric`**: public-key signatures (Ed25519, ECDSA, RSA). You store the provider's public key instead of a shared secret. + +Once a request is verified, see [Connecting a provider](/webhooks/connect) for how to point the +provider at the webhook URL and set the secret. diff --git a/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql b/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql new file mode 100644 index 00000000000..862217db382 --- /dev/null +++ b/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql @@ -0,0 +1,33 @@ +-- +goose Up + +CREATE TABLE IF NOT EXISTS trigger_dev.webhook_deliveries_v1 +( + environment_id String, + organization_id String, + project_id String, + delivery_id String, + webhook_endpoint_id String, + + environment_type LowCardinality(String), + friendly_id String, + + external_delivery_id String DEFAULT '', + run_id String DEFAULT '', + + status LowCardinality(String), + + is_test UInt8 DEFAULT 0, + + created_at DateTime64(3), + updated_at DateTime64(3), + + _version UInt64, + _is_deleted UInt8 DEFAULT 0 +) +ENGINE = ReplacingMergeTree(_version, _is_deleted) +PARTITION BY toYYYYMM(created_at) +ORDER BY (organization_id, project_id, environment_id, created_at, delivery_id) +-- 60d retention aligned with the Postgres partition retention. ttl_only_drop_parts makes expiry a +-- whole-part drop (monthly partitions); materialize_ttl_recalculate_only keeps TTL lightweight. +TTL toDateTime(created_at) + INTERVAL 60 DAY +SETTINGS ttl_only_drop_parts = 1, materialize_ttl_recalculate_only = 1; diff --git a/internal-packages/clickhouse/src/index.ts b/internal-packages/clickhouse/src/index.ts index bc8b6e662c8..b9b735a7456 100644 --- a/internal-packages/clickhouse/src/index.ts +++ b/internal-packages/clickhouse/src/index.ts @@ -39,6 +39,12 @@ import { getSessionsQueryBuilder, insertSessionsCompactArrays, } from "./sessions.js"; +import { + getWebhookDeliveriesQueryBuilder, + getWebhookDeliveriesCountQueryBuilder, + getWebhookDeliveriesGroupedCountQueryBuilder, + insertWebhookDeliveriesCompactArrays, +} from "./webhookDeliveries.js"; import { getGlobalModelMetrics, getGlobalModelComparison, @@ -69,6 +75,7 @@ export type * from "./llmMetrics.js"; export type * from "./llmModelAggregates.js"; export type * from "./errors.js"; export type * from "./sessions.js"; +export type * from "./webhookDeliveries.js"; export type * from "./client/queryBuilder.js"; // Re-export column constants, indices, and type-safe accessors @@ -83,6 +90,11 @@ export { } from "./taskRuns.js"; export { SESSION_COLUMNS, SESSION_INDEX, getSessionField } from "./sessions.js"; +export { + WEBHOOK_DELIVERY_COLUMNS, + WEBHOOK_DELIVERY_INDEX, + getWebhookDeliveryField, +} from "./webhookDeliveries.js"; // TSQL query execution export { @@ -279,6 +291,15 @@ export class ClickHouse { }; } + get webhookDeliveries() { + return { + insertCompactArrays: insertWebhookDeliveriesCompactArrays(this.writer), + queryBuilder: getWebhookDeliveriesQueryBuilder(this.reader), + countQueryBuilder: getWebhookDeliveriesCountQueryBuilder(this.reader), + groupedCountQueryBuilder: getWebhookDeliveriesGroupedCountQueryBuilder(this.reader), + }; + } + get taskEventsV2() { return { insert: insertTaskEventsV2(this.writer), diff --git a/internal-packages/clickhouse/src/webhookDeliveries.ts b/internal-packages/clickhouse/src/webhookDeliveries.ts new file mode 100644 index 00000000000..cf6554969a0 --- /dev/null +++ b/internal-packages/clickhouse/src/webhookDeliveries.ts @@ -0,0 +1,155 @@ +import { ClickHouseSettings } from "@clickhouse/client"; +import { z } from "zod"; +import { ClickhouseReader, ClickhouseWriter } from "./client/types.js"; + +export const WebhookDeliveryV1 = z.object({ + environment_id: z.string(), + organization_id: z.string(), + project_id: z.string(), + delivery_id: z.string(), + webhook_endpoint_id: z.string(), + environment_type: z.string(), + friendly_id: z.string(), + external_delivery_id: z.string().default(""), + run_id: z.string().default(""), + status: z.string(), + is_test: z.number().int().default(0), + created_at: z.number().int(), + updated_at: z.number().int(), + _version: z.string(), + _is_deleted: z.number().int().default(0), +}); +export type WebhookDeliveryV1 = z.input; + +export const WEBHOOK_DELIVERY_COLUMNS = [ + "environment_id", + "organization_id", + "project_id", + "delivery_id", + "webhook_endpoint_id", + "environment_type", + "friendly_id", + "external_delivery_id", + "run_id", + "status", + "is_test", + "created_at", + "updated_at", + "_version", + "_is_deleted", +] as const; + +export type WebhookDeliveryColumnName = (typeof WEBHOOK_DELIVERY_COLUMNS)[number]; + +export const WEBHOOK_DELIVERY_INDEX = Object.fromEntries( + WEBHOOK_DELIVERY_COLUMNS.map((col, idx) => [col, idx]) +) as { readonly [K in WebhookDeliveryColumnName]: number }; + +export type WebhookDeliveryFieldTypes = { + environment_id: string; + organization_id: string; + project_id: string; + delivery_id: string; + webhook_endpoint_id: string; + environment_type: string; + friendly_id: string; + external_delivery_id: string; + run_id: string; + status: string; + is_test: number; + created_at: number; + updated_at: number; + _version: string; + _is_deleted: number; +}; + +export type WebhookDeliveryInsertArray = [ + environment_id: string, + organization_id: string, + project_id: string, + delivery_id: string, + webhook_endpoint_id: string, + environment_type: string, + friendly_id: string, + external_delivery_id: string, + run_id: string, + status: string, + is_test: number, + created_at: number, + updated_at: number, + _version: string, + _is_deleted: number, +]; + +export function getWebhookDeliveryField( + row: WebhookDeliveryInsertArray, + field: K +): WebhookDeliveryFieldTypes[K] { + return row[WEBHOOK_DELIVERY_INDEX[field]] as WebhookDeliveryFieldTypes[K]; +} + +export function insertWebhookDeliveriesCompactArrays( + ch: ClickhouseWriter, + settings?: ClickHouseSettings +) { + return ch.insertCompactRaw({ + name: "insertWebhookDeliveriesCompactArrays", + table: "trigger_dev.webhook_deliveries_v1", + columns: WEBHOOK_DELIVERY_COLUMNS, + settings, // no enable_json_type + }); +} + +export const WebhookDeliveryV1QueryResult = z.object({ + delivery_id: z.string(), + created_at_ms: z.number().int(), +}); +export type WebhookDeliveryV1QueryResult = z.infer; + +export function getWebhookDeliveriesQueryBuilder( + ch: ClickhouseReader, + settings?: ClickHouseSettings +) { + return ch.queryBuilder({ + name: "getWebhookDeliveries", + baseQuery: + "SELECT delivery_id, toUnixTimestamp64Milli(created_at) AS created_at_ms FROM trigger_dev.webhook_deliveries_v1 FINAL", + schema: WebhookDeliveryV1QueryResult, + settings, + }); +} + +export function getWebhookDeliveriesCountQueryBuilder( + ch: ClickhouseReader, + settings?: ClickHouseSettings +) { + return ch.queryBuilder({ + name: "getWebhookDeliveriesCount", + baseQuery: "SELECT count() as count FROM trigger_dev.webhook_deliveries_v1 FINAL", + schema: z.object({ count: z.number().int() }), + settings, + }); +} + +export const WebhookDeliveryGroupedCountResult = z.object({ + webhook_endpoint_id: z.string(), + count: z.number().int(), +}); +export type WebhookDeliveryGroupedCountResult = z.infer; + +// Per-endpoint delivery counts in one query (caller adds the scope/period WHERE + GROUP BY). Uses +// count(DISTINCT delivery_id) instead of FINAL: it dedupes the ReplacingMergeTree version rows (one +// per status transition) without the full-merge cost of FINAL, which matters when counting per +// endpoint for a list. +export function getWebhookDeliveriesGroupedCountQueryBuilder( + ch: ClickhouseReader, + settings?: ClickHouseSettings +) { + return ch.queryBuilder({ + name: "getWebhookDeliveriesGroupedCount", + baseQuery: + "SELECT webhook_endpoint_id, count(DISTINCT delivery_id) AS count FROM trigger_dev.webhook_deliveries_v1", + schema: WebhookDeliveryGroupedCountResult, + settings, + }); +} diff --git a/internal-packages/database/prisma/migrations/20260622120730_add_webhook_trigger_source/migration.sql b/internal-packages/database/prisma/migrations/20260622120730_add_webhook_trigger_source/migration.sql new file mode 100644 index 00000000000..6bdb9473703 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260622120730_add_webhook_trigger_source/migration.sql @@ -0,0 +1,5 @@ +-- AlterEnum +-- Standalone enum-only migration (Q5): the new value is committed before any +-- table migration or code references it, so the wrapped-transaction restriction +-- on using a freshly added enum value never bites. +ALTER TYPE "public"."TaskTriggerSource" ADD VALUE 'WEBHOOK'; diff --git a/internal-packages/database/prisma/migrations/20260622120731_add_webhook_endpoint_and_delivery/migration.sql b/internal-packages/database/prisma/migrations/20260622120731_add_webhook_endpoint_and_delivery/migration.sql new file mode 100644 index 00000000000..27115888e7d --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260622120731_add_webhook_endpoint_and_delivery/migration.sql @@ -0,0 +1,76 @@ +-- CreateEnum +CREATE TYPE "public"."WebhookEndpointStatus" AS ENUM ('ACTIVE', 'INACTIVE', 'DELETING'); + +-- CreateEnum +CREATE TYPE "public"."WebhookDeliveryStatus" AS ENUM ('PENDING', 'PROCESSING', 'SUCCEEDED', 'FAILED'); + +-- NOTE (Q5): ALTER TYPE "TaskTriggerSource" ADD VALUE 'WEBHOOK' lives in the earlier +-- 20260622120730_add_webhook_trigger_source migration, not here. + +-- CreateTable +CREATE TABLE "public"."WebhookEndpoint" ( + "id" TEXT NOT NULL, + "friendlyId" TEXT NOT NULL, + "opaqueId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "runtimeEnvironmentId" TEXT NOT NULL, + "environmentType" "public"."RuntimeEnvironmentType" NOT NULL, + "endpointTenantId" TEXT NOT NULL DEFAULT '', + "endpointExternalRef" TEXT NOT NULL DEFAULT '', + "source" TEXT NOT NULL, + "handlerWebhookId" TEXT NOT NULL, + "routingTarget" JSONB NOT NULL, + "verifierArtifact" JSONB NOT NULL, + "metadata" JSONB NOT NULL DEFAULT '{}', + "signingSecretKey" TEXT, + "status" "public"."WebhookEndpointStatus" NOT NULL DEFAULT 'ACTIVE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WebhookEndpoint_pkey" PRIMARY KEY ("id") +); + +-- CreateTable (HAND-EDITED: PARTITION BY RANGE clause; clone of TaskEventPartitioned migration.sql:54) +CREATE TABLE "public"."WebhookDelivery" ( + "id" TEXT NOT NULL, + "friendlyId" TEXT NOT NULL, + "webhookEndpointId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "runtimeEnvironmentId" TEXT NOT NULL, + "environmentType" "public"."RuntimeEnvironmentType" NOT NULL, + "externalDeliveryId" TEXT NOT NULL, + "idempotencyKey" TEXT NOT NULL, + "runId" TEXT, + "status" "public"."WebhookDeliveryStatus" NOT NULL DEFAULT 'PENDING', + "isTest" BOOLEAN NOT NULL DEFAULT false, + "parsedEvent" JSONB, + "rawBodyHash" TEXT, + "errorMessage" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "processedAt" TIMESTAMP(3), + + CONSTRAINT "WebhookDelivery_pkey" PRIMARY KEY ("id","createdAt") +) PARTITION BY RANGE ("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "WebhookEndpoint_friendlyId_key" ON "public"."WebhookEndpoint"("friendlyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "WebhookEndpoint_opaqueId_key" ON "public"."WebhookEndpoint"("opaqueId"); + +-- CreateIndex +CREATE INDEX "WebhookEndpoint_runtimeEnvironmentId_source_idx" ON "public"."WebhookEndpoint"("runtimeEnvironmentId", "source"); + +-- CreateIndex +CREATE INDEX "WebhookEndpoint_projectId_createdAt_idx" ON "public"."WebhookEndpoint"("projectId", "createdAt" DESC); + +-- CreateIndex +CREATE UNIQUE INDEX "WebhookEndpoint_runtimeEnvironmentId_handlerWebhookId_endpo_key" ON "public"."WebhookEndpoint"("runtimeEnvironmentId", "handlerWebhookId", "endpointTenantId", "endpointExternalRef"); + +-- CreateIndex (on the PARENT; auto-propagates to every child at PARTITION OF time) +CREATE INDEX "WebhookDelivery_webhookEndpointId_createdAt_idx" ON "public"."WebhookDelivery"("webhookEndpointId", "createdAt" DESC); + +-- No DEFAULT partition on purpose: retention drops use DETACH ... CONCURRENTLY, which a default forbids. diff --git a/internal-packages/database/prisma/migrations/20260622130000_add_webhook_delivery_headers/migration.sql b/internal-packages/database/prisma/migrations/20260622130000_add_webhook_delivery_headers/migration.sql new file mode 100644 index 00000000000..88e702fd054 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260622130000_add_webhook_delivery_headers/migration.sql @@ -0,0 +1,4 @@ +-- Add the inbound request headers to webhook deliveries. Surfaced to the webhook task via +-- onEvent({ headers }). Nullable + additive; the ADD COLUMN on the RANGE-partitioned parent +-- cascades to all child partitions automatically. +ALTER TABLE "WebhookDelivery" ADD COLUMN "headers" JSONB; diff --git a/internal-packages/database/prisma/migrations/20260622140000_add_webhook_endpoint_secret_provisioning/migration.sql b/internal-packages/database/prisma/migrations/20260622140000_add_webhook_endpoint_secret_provisioning/migration.sql new file mode 100644 index 00000000000..49ef2aa160d --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260622140000_add_webhook_endpoint_secret_provisioning/migration.sql @@ -0,0 +1,3 @@ +-- Who supplies the signing secret/key for a webhook endpoint ("provider" | "integrator" | "either"). +-- Drives the dashboard Connect UI (paste vs generate). Synced from the declared source. +ALTER TABLE "WebhookEndpoint" ADD COLUMN "secretProvisioning" TEXT NOT NULL DEFAULT 'either'; diff --git a/internal-packages/database/prisma/migrations/20260630120000_add_webhook_delivery_filter/migration.sql b/internal-packages/database/prisma/migrations/20260630120000_add_webhook_delivery_filter/migration.sql new file mode 100644 index 00000000000..b2166ea824d --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260630120000_add_webhook_delivery_filter/migration.sql @@ -0,0 +1,11 @@ +-- Webhook delivery filters: a server-side predicate gates routing (not receipt). A verified delivery +-- that doesn't match becomes a FILTERED row (no run/session) with the reason recorded; the compiled +-- filter AST is stored on the endpoint and evaluated at ingest. + +ALTER TYPE "WebhookDeliveryStatus" ADD VALUE 'FILTERED'; + +ALTER TABLE "WebhookDelivery" ADD COLUMN "filterReason" TEXT; + +ALTER TABLE "WebhookEndpoint" ADD COLUMN "filter" TEXT; +ALTER TABLE "WebhookEndpoint" ADD COLUMN "filterAst" JSONB; +ALTER TABLE "WebhookEndpoint" ADD COLUMN "filterAstVersion" INTEGER; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 2800dd5dd2c..79240c8e108 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -733,6 +733,98 @@ enum TaskTriggerSource { STANDARD SCHEDULED AGENT + WEBHOOK +} + +model WebhookEndpoint { + id String @id @default(cuid()) + friendlyId String @unique // wh_... (WebhookEndpointId IdUtil "wh") + + /// URL key. CSPRNG, NOT a friendlyId: crypto.randomBytes(16) base64url. + /// Generated by the create path (deploy-sync / P2 API), NOT by @default. + opaqueId String @unique // global unique (Q2): ingress resolves the endpoint by opaqueId alone + + // scope (plain columns, NO @relation -> no FK, matching TaskEventPartitioned) + organizationId String + projectId String + runtimeEnvironmentId String + environmentType RuntimeEnvironmentType // denormalized from the env at deploy-sync (immutable per endpoint), Q3 + + // tenant scope. "" sentinel on declared (P1); P2 dynamic create fills these. + // Sentinel not null so the 4-column @@unique actually bites. + endpointTenantId String @default("") + endpointExternalRef String @default("") + + source String // provider tag e.g. "stripe","slack","github" + handlerWebhookId String // declared webhook() id (string ref, GOLDEN LAW, no relation) + routingTarget Json // RoutingTarget tagged union ({ type: "task" } | { type: "session" }) + verifierArtifact Json // VerifierArtifact tagged union (config|preset in P1) + filter String? // source filter DSL string (display/round-trip) + filterAst Json? // compiled FilterAst, evaluated at ingest; null = route all + filterAstVersion Int? // re-parse `filter` on a format bump + metadata Json @default("{}") // arbitrary user metadata; flows into the webhook task + + // who supplies the secret/key; drives the Connect UI (paste vs generate). From the source. + secretProvisioning String @default("either") // "provider" | "integrator" | "either" + + /// SecretReference.key string. Plain String, NO @relation -> no FK to SecretReference. + signingSecretKey String? + + status WebhookEndpointStatus @default(ACTIVE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([runtimeEnvironmentId, handlerWebhookId, endpointTenantId, endpointExternalRef]) // deploy-sync key + @@index([runtimeEnvironmentId, source]) + @@index([projectId, createdAt(sort: Desc)]) +} + +enum WebhookEndpointStatus { + ACTIVE + INACTIVE + DELETING +} + +model WebhookDelivery { + id String @default(cuid()) + friendlyId String // whd_... (WebhookDeliveryId IdUtil "whd"). Uniqueness scoped by the composite PK. + + // plain columns, NO @relation -> no FK + webhookEndpointId String + organizationId String + projectId String + runtimeEnvironmentId String + environmentType RuntimeEnvironmentType + + externalDeliveryId String // provider id (Stripe event id, GitHub X-GitHub-Delivery) + idempotencyKey String // = externalDeliveryId; passed to Run Engine when routing to a task + runId String? // the triggered run, if task target + + status WebhookDeliveryStatus @default(PENDING) + + /// Set from the x-trigger-test ingress header; marks console/test-send deliveries so the list can filter them. + isTest Boolean @default(false) + + parsedEvent Json? // size-capped snapshot of the verified event (full event lives in ClickHouse) + headers Json? // inbound request headers, surfaced to the webhook task via onEvent({ headers }) + rawBodyHash String? // sha256 of raw bytes; cheap P2 replay anchor + errorMessage String? + filterReason String? // why a FILTERED delivery was not routed (failing clause + actual value) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Q14: real column; the status transitions update it, maps straight to CH updated_at + processedAt DateTime? + + @@id([id, createdAt]) // composite PK: the partition key MUST be in every PK/unique on a partitioned table + @@index([webhookEndpointId, createdAt(sort: Desc)]) // dashboard recent-deliveries; also partition-pruned +} + +enum WebhookDeliveryStatus { + PENDING + PROCESSING + SUCCEEDED + FAILED + FILTERED } model PlaygroundConversation { diff --git a/internal-packages/database/src/transaction.ts b/internal-packages/database/src/transaction.ts index fad07d1102b..d6de5ba7bff 100644 --- a/internal-packages/database/src/transaction.ts +++ b/internal-packages/database/src/transaction.ts @@ -18,6 +18,25 @@ export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient; export type PrismaReplicaClient = Omit; +// Narrow client views for the webhook feature's tables, prepping it to run on a +// dedicated Postgres: control-plane models are absent, so touching one on the +// webhook client (or spanning both DBs in a $transaction) is a compile error. +export type WebhookDatabase = Pick< + PrismaClient, + | "webhookEndpoint" + | "webhookDelivery" + | "$transaction" + | "$queryRaw" + | "$queryRawUnsafe" + | "$executeRaw" + | "$executeRawUnsafe" +>; + +export type WebhookReplicaDatabase = Pick< + PrismaReplicaClient, + "webhookEndpoint" | "webhookDelivery" | "$queryRaw" | "$queryRawUnsafe" +>; + export { Decimal }; function isTransactionClient(prisma: PrismaClientOrTransaction): prisma is PrismaTransactionClient { diff --git a/internal-packages/llm-model-catalog/src/defaultPrices.ts b/internal-packages/llm-model-catalog/src/defaultPrices.ts index 982b6b2ec15..fb347c2bef6 100644 --- a/internal-packages/llm-model-catalog/src/defaultPrices.ts +++ b/internal-packages/llm-model-catalog/src/defaultPrices.ts @@ -6,3106 +6,3091 @@ import type { DefaultModelDefinition } from "./types.js"; export const defaultModelPrices: DefaultModelDefinition[] = [ { - modelName: "gpt-4o", - matchPattern: "(?i)^(openai/)?(gpt-4o)$", - startDate: "2024-05-13T23:15:07.670Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - input_cached_tokens: 0.00000125, - input_cache_read: 0.00000125, - output: 0.00001, - }, - }, - ], - }, - { - modelName: "gpt-4o-2024-05-13", - matchPattern: "(?i)^(openai/)?(gpt-4o-2024-05-13)$", - startDate: "2024-05-13T23:15:07.670Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000005, - output: 0.000015, - }, - }, - ], - }, - { - modelName: "gpt-4-1106-preview", - matchPattern: "(?i)^(openai/)?(gpt-4-1106-preview)$", - startDate: "2024-04-23T10:37:17.092Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "gpt-4-turbo-vision", - matchPattern: "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "gpt-4-32k", - matchPattern: "(?i)^(openai/)?(gpt-4-32k)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00006, - output: 0.00012, - }, - }, - ], - }, - { - modelName: "gpt-4-32k-0613", - matchPattern: "(?i)^(openai/)?(gpt-4-32k-0613)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00006, - output: 0.00012, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-1106", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000001, - output: 0.000002, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-0613", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000015, - output: 0.000002, - }, - }, - ], - }, - { - modelName: "gpt-4-0613", - matchPattern: "(?i)^(openai/)?(gpt-4-0613)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00003, - output: 0.00006, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-instruct", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000015, - output: 0.000002, - }, - }, - ], - }, - { - modelName: "text-ada-001", - matchPattern: "(?i)^(text-ada-001)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 0.000004, - }, - }, - ], - }, - { - modelName: "text-babbage-001", - matchPattern: "(?i)^(text-babbage-001)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 5e-7, - }, - }, - ], - }, - { - modelName: "text-curie-001", - matchPattern: "(?i)^(text-curie-001)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 0.00002, - }, - }, - ], - }, - { - modelName: "text-davinci-001", - matchPattern: "(?i)^(text-davinci-001)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 0.00002, - }, - }, - ], - }, - { - modelName: "text-davinci-002", - matchPattern: "(?i)^(text-davinci-002)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 0.00002, - }, - }, - ], - }, - { - modelName: "text-davinci-003", - matchPattern: "(?i)^(text-davinci-003)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 0.00002, - }, - }, - ], - }, - { - modelName: "text-embedding-ada-002-v2", - matchPattern: "(?i)^(text-embedding-ada-002-v2)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 1e-7, - }, - }, - ], - }, - { - modelName: "text-embedding-ada-002", - matchPattern: "(?i)^(text-embedding-ada-002)$", - startDate: "2024-01-24T18:18:50.861Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 1e-7, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-16k-0613", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$", - startDate: "2024-02-03T17:29:57.350Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - output: 0.000004, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-0301", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - output: 0.000002, - }, - }, - ], - }, - { - modelName: "gpt-4-32k-0314", - matchPattern: "(?i)^(openai/)?(gpt-4-32k-0314)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00006, - output: 0.00012, - }, - }, - ], - }, - { - modelName: "gpt-4-0314", - matchPattern: "(?i)^(openai/)?(gpt-4-0314)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00003, - output: 0.00006, - }, - }, - ], - }, - { - modelName: "gpt-4", - matchPattern: "(?i)^(openai/)?(gpt-4)$", - startDate: "2024-01-24T10:19:21.693Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00003, - output: 0.00006, - }, - }, - ], - }, - { - modelName: "claude-instant-1.2", - matchPattern: "(?i)^(anthropic/)?(claude-instant-1.2)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000163, - output: 0.00000551, - }, - }, - ], - }, - { - modelName: "claude-2.0", - matchPattern: "(?i)^(anthropic/)?(claude-2.0)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000008, - output: 0.000024, - }, - }, - ], - }, - { - modelName: "claude-2.1", - matchPattern: "(?i)^(anthropic/)?(claude-2.1)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000008, - output: 0.000024, - }, - }, - ], - }, - { - modelName: "claude-1.3", - matchPattern: "(?i)^(anthropic/)?(claude-1.3)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000008, - output: 0.000024, - }, - }, - ], - }, - { - modelName: "claude-1.2", - matchPattern: "(?i)^(anthropic/)?(claude-1.2)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000008, - output: 0.000024, - }, - }, - ], - }, - { - modelName: "claude-1.1", - matchPattern: "(?i)^(anthropic/)?(claude-1.1)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000008, - output: 0.000024, - }, - }, - ], - }, - { - modelName: "claude-instant-1", - matchPattern: "(?i)^(anthropic/)?(claude-instant-1)$", - startDate: "2024-01-30T15:44:13.447Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000163, - output: 0.00000551, - }, - }, - ], - }, - { - modelName: "babbage-002", - matchPattern: "(?i)^(babbage-002)$", - startDate: "2024-01-26T17:35:21.129Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 4e-7, - output: 0.0000016, - }, - }, - ], - }, - { - modelName: "davinci-002", - matchPattern: "(?i)^(davinci-002)$", - startDate: "2024-01-26T17:35:21.129Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000006, - output: 0.000012, - }, - }, - ], - }, - { - modelName: "text-embedding-3-small", - matchPattern: "(?i)^(text-embedding-3-small)$", - startDate: "2024-01-26T17:35:21.129Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 2e-8, - }, - }, - ], - }, - { - modelName: "text-embedding-3-large", - matchPattern: "(?i)^(text-embedding-3-large)$", - startDate: "2024-01-26T17:35:21.129Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 1.3e-7, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-0125", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$", - startDate: "2024-01-26T17:35:21.129Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 5e-7, - output: 0.0000015, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$", - startDate: "2024-02-13T12:00:37.424Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 5e-7, - output: 0.0000015, - }, - }, - ], - }, - { - modelName: "gpt-4-0125-preview", - matchPattern: "(?i)^(openai/)?(gpt-4-0125-preview)$", - startDate: "2024-01-26T17:35:21.129Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "ft:gpt-3.5-turbo-1106", - matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - output: 0.000006, - }, - }, - ], - }, - { - modelName: "ft:gpt-3.5-turbo-0613", - matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000012, - output: 0.000016, - }, - }, - ], - }, - { - modelName: "ft:davinci-002", - matchPattern: "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000012, - output: 0.000012, - }, - }, - ], - }, - { - modelName: "ft:babbage-002", - matchPattern: "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000016, - output: 0.0000016, - }, - }, - ], - }, - { - modelName: "chat-bison", - matchPattern: "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "codechat-bison-32k", - matchPattern: "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "codechat-bison", - matchPattern: "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "text-bison-32k", - matchPattern: "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "chat-bison-32k", - matchPattern: "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "text-unicorn", - matchPattern: "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - output: 0.0000075, - }, - }, - ], - }, - { - modelName: "text-bison", - matchPattern: "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "textembedding-gecko", - matchPattern: "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 1e-7, - }, - }, - ], - }, - { - modelName: "textembedding-gecko-multilingual", - matchPattern: "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - total: 1e-7, - }, - }, - ], - }, - { - modelName: "code-gecko", - matchPattern: "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "code-bison", - matchPattern: "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "code-bison-32k", - matchPattern: "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$", - startDate: "2024-01-31T13:25:02.141Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "gpt-3.5-turbo-16k", - matchPattern: "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$", - startDate: "2024-02-13T12:00:37.424Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 5e-7, - output: 0.0000015, - }, - }, - ], - }, - { - modelName: "gpt-4-turbo-preview", - matchPattern: "(?i)^(openai/)?(gpt-4-turbo-preview)$", - startDate: "2024-02-15T21:21:50.947Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "claude-3-opus-20240229", - matchPattern: - "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$", - startDate: "2024-03-07T17:55:38.139Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - output: 0.000075, - }, - }, - ], - }, - { - modelName: "claude-3-sonnet-20240229", - matchPattern: - "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$", - startDate: "2024-03-07T17:55:38.139Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-3-haiku-20240307", - matchPattern: - "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$", - startDate: "2024-03-14T09:41:18.736Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 0.00000125, - }, - }, - ], - }, - { - modelName: "gemini-1.0-pro-latest", - matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$", - startDate: "2024-04-11T10:27:46.517Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - output: 5e-7, - }, - }, - ], - }, - { - modelName: "gemini-1.0-pro", - matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$", - startDate: "2024-04-11T10:27:46.517Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1.25e-7, - output: 3.75e-7, - }, - }, - ], - }, - { - modelName: "gemini-1.0-pro-001", - matchPattern: "(?i)^(google(ai)?/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$", - startDate: "2024-04-11T10:27:46.517Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1.25e-7, - output: 3.75e-7, - }, - }, - ], - }, - { - modelName: "gemini-pro", - matchPattern: "(?i)^(google(ai)?/)?(gemini-pro)(@[a-zA-Z0-9]+)?$", - startDate: "2024-04-11T10:27:46.517Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1.25e-7, - output: 3.75e-7, - }, - }, - ], - }, - { - modelName: "gemini-1.5-pro-latest", - matchPattern: "(?i)^(google(ai)?/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$", - startDate: "2024-04-11T10:27:46.517Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - output: 0.0000075, - }, - }, - ], - }, - { - modelName: "gpt-4-turbo-2024-04-09", - matchPattern: "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$", - startDate: "2024-04-23T10:37:17.092Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "gpt-4-turbo", - matchPattern: "(?i)^(openai/)?(gpt-4-turbo)$", - startDate: "2024-04-11T21:13:44.989Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "gpt-4-preview", - matchPattern: "(?i)^(openai/)?(gpt-4-preview)$", - startDate: "2024-04-23T10:37:17.092Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00001, - output: 0.00003, - }, - }, - ], - }, - { - modelName: "claude-3-5-sonnet-20240620", - matchPattern: - "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$", - startDate: "2024-06-25T11:47:24.475Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "gpt-4o-mini", - matchPattern: "(?i)^(openai/)?(gpt-4o-mini)$", - startDate: "2024-07-18T17:56:09.591Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1.5e-7, - output: 6e-7, - input_cached_tokens: 7.5e-8, - input_cache_read: 7.5e-8, - }, - }, - ], - }, - { - modelName: "gpt-4o-mini-2024-07-18", - matchPattern: "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$", - startDate: "2024-07-18T17:56:09.591Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1.5e-7, - input_cached_tokens: 7.5e-8, - input_cache_read: 7.5e-8, - output: 6e-7, - }, - }, - ], - }, - { - modelName: "gpt-4o-2024-08-06", - matchPattern: "(?i)^(openai/)?(gpt-4o-2024-08-06)$", - startDate: "2024-08-07T11:54:31.298Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - input_cached_tokens: 0.00000125, - input_cache_read: 0.00000125, - output: 0.00001, - }, - }, - ], - }, - { - modelName: "o1-preview", - matchPattern: "(?i)^(openai/)?(o1-preview)$", - startDate: "2024-09-13T10:01:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - input_cached_tokens: 0.0000075, - input_cache_read: 0.0000075, - output: 0.00006, - output_reasoning_tokens: 0.00006, - output_reasoning: 0.00006, - }, - }, - ], - }, - { - modelName: "o1-preview-2024-09-12", - matchPattern: "(?i)^(openai/)?(o1-preview-2024-09-12)$", - startDate: "2024-09-13T10:01:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - input_cached_tokens: 0.0000075, - input_cache_read: 0.0000075, - output: 0.00006, - output_reasoning_tokens: 0.00006, - output_reasoning: 0.00006, - }, - }, - ], - }, - { - modelName: "o1-mini", - matchPattern: "(?i)^(openai/)?(o1-mini)$", - startDate: "2024-09-13T10:01:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000011, - input_cached_tokens: 5.5e-7, - input_cache_read: 5.5e-7, - output: 0.0000044, - output_reasoning_tokens: 0.0000044, - output_reasoning: 0.0000044, - }, - }, - ], - }, - { - modelName: "o1-mini-2024-09-12", - matchPattern: "(?i)^(openai/)?(o1-mini-2024-09-12)$", - startDate: "2024-09-13T10:01:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000011, - input_cached_tokens: 5.5e-7, - input_cache_read: 5.5e-7, - output: 0.0000044, - output_reasoning_tokens: 0.0000044, - output_reasoning: 0.0000044, - }, - }, - ], - }, - { - modelName: "claude-3.5-sonnet-20241022", - matchPattern: - "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$", - startDate: "2024-10-22T18:48:01.676Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-3.5-sonnet-latest", - matchPattern: "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$", - startDate: "2024-10-22T18:48:01.676Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-3-5-haiku-20241022", - matchPattern: - "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$", - startDate: "2024-11-05T10:30:50.566Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 8e-7, - input_tokens: 8e-7, - output: 0.000004, - output_tokens: 0.000004, - cache_creation_input_tokens: 0.000001, - input_cache_creation: 0.000001, - input_cache_creation_5m: 0.000001, - input_cache_creation_1h: 0.0000016, - cache_read_input_tokens: 8e-8, - input_cache_read: 8e-8, - }, - }, - ], - }, - { - modelName: "claude-3.5-haiku-latest", - matchPattern: "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$", - startDate: "2024-11-05T10:30:50.566Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 8e-7, - input_tokens: 8e-7, - output: 0.000004, - output_tokens: 0.000004, - cache_creation_input_tokens: 0.000001, - input_cache_creation: 0.000001, - input_cache_creation_5m: 0.000001, - input_cache_creation_1h: 0.0000016, - cache_read_input_tokens: 8e-8, - input_cache_read: 8e-8, - }, - }, - ], - }, - { - modelName: "chatgpt-4o-latest", - matchPattern: "(?i)^(chatgpt-4o-latest)$", - startDate: "2024-11-25T12:47:17.504Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000005, - output: 0.000015, - }, - }, - ], - }, - { - modelName: "gpt-4o-2024-11-20", - matchPattern: "(?i)^(openai/)?(gpt-4o-2024-11-20)$", - startDate: "2024-12-03T10:06:12.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - input_cached_tokens: 0.00000125, - input_cache_read: 0.00000125, - output: 0.00001, - }, - }, - ], - }, - { - modelName: "gpt-4o-audio-preview", - matchPattern: "(?i)^(openai/)?(gpt-4o-audio-preview)$", - startDate: "2024-12-03T10:19:56.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input_text_tokens: 0.0000025, - output_text_tokens: 0.00001, - input_audio_tokens: 0.0001, - input_audio: 0.0001, - output_audio_tokens: 0.0002, - output_audio: 0.0002, - }, - }, - ], - }, - { - modelName: "gpt-4o-audio-preview-2024-10-01", - matchPattern: "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$", - startDate: "2024-12-03T10:19:56.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input_text_tokens: 0.0000025, - output_text_tokens: 0.00001, - input_audio_tokens: 0.0001, - input_audio: 0.0001, - output_audio_tokens: 0.0002, - output_audio: 0.0002, - }, - }, - ], - }, - { - modelName: "gpt-4o-realtime-preview", - matchPattern: "(?i)^(openai/)?(gpt-4o-realtime-preview)$", - startDate: "2024-12-03T10:19:56.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input_text_tokens: 0.000005, - input_cached_text_tokens: 0.0000025, - output_text_tokens: 0.00002, - input_audio_tokens: 0.0001, - input_audio: 0.0001, - input_cached_audio_tokens: 0.00002, - output_audio_tokens: 0.0002, - output_audio: 0.0002, - }, - }, - ], - }, - { - modelName: "gpt-4o-realtime-preview-2024-10-01", - matchPattern: "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$", - startDate: "2024-12-03T10:19:56.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input_text_tokens: 0.000005, - input_cached_text_tokens: 0.0000025, - output_text_tokens: 0.00002, - input_audio_tokens: 0.0001, - input_audio: 0.0001, - input_cached_audio_tokens: 0.00002, - output_audio_tokens: 0.0002, - output_audio: 0.0002, - }, - }, - ], - }, - { - modelName: "o1", - matchPattern: "(?i)^(openai/)?(o1)$", - startDate: "2025-01-17T00:01:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - input_cached_tokens: 0.0000075, - input_cache_read: 0.0000075, - output: 0.00006, - output_reasoning_tokens: 0.00006, - output_reasoning: 0.00006, - }, - }, - ], - }, - { - modelName: "o1-2024-12-17", - matchPattern: "(?i)^(openai/)?(o1-2024-12-17)$", - startDate: "2025-01-17T00:01:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - input_cached_tokens: 0.0000075, - input_cache_read: 0.0000075, - output: 0.00006, - output_reasoning_tokens: 0.00006, - output_reasoning: 0.00006, - }, - }, - ], - }, - { - modelName: "o3-mini", - matchPattern: "(?i)^(openai/)?(o3-mini)$", - startDate: "2025-01-31T20:41:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000011, - input_cached_tokens: 5.5e-7, - input_cache_read: 5.5e-7, - output: 0.0000044, - output_reasoning_tokens: 0.0000044, - output_reasoning: 0.0000044, - }, - }, - ], - }, - { - modelName: "o3-mini-2025-01-31", - matchPattern: "(?i)^(openai/)?(o3-mini-2025-01-31)$", - startDate: "2025-01-31T20:41:35.373Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000011, - input_cached_tokens: 5.5e-7, - input_cache_read: 5.5e-7, - output: 0.0000044, - output_reasoning_tokens: 0.0000044, - output_reasoning: 0.0000044, - }, - }, - ], - }, - { - modelName: "gemini-2.0-flash-001", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$", - startDate: "2025-02-06T11:11:35.241Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1e-7, - output: 4e-7, - }, - }, - ], - }, - { - modelName: "gemini-2.0-flash-lite-preview-02-05", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$", - startDate: "2025-02-06T11:11:35.241Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 7.5e-8, - output: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-3.7-sonnet-20250219", - matchPattern: - "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$", - startDate: "2025-02-25T09:35:39.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-3.7-sonnet-latest", - matchPattern: "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$", - startDate: "2025-02-25T09:35:39.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "gpt-4.5-preview", - matchPattern: "(?i)^(openai/)?(gpt-4.5-preview)$", - startDate: "2025-02-27T21:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000075, - input_cached_tokens: 0.0000375, - input_cached_text_tokens: 0.0000375, - input_cache_read: 0.0000375, - output: 0.00015, - }, - }, - ], - }, - { - modelName: "gpt-4.5-preview-2025-02-27", - matchPattern: "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$", - startDate: "2025-02-27T21:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000075, - input_cached_tokens: 0.0000375, - input_cached_text_tokens: 0.0000375, - input_cache_read: 0.0000375, - output: 0.00015, - }, - }, - ], - }, - { - modelName: "gpt-4.1", - matchPattern: "(?i)^(openai/)?(gpt-4.1)$", - startDate: "2025-04-15T10:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - input_cached_tokens: 5e-7, - input_cached_text_tokens: 5e-7, - input_cache_read: 5e-7, - output: 0.000008, - }, - }, - ], - }, - { - modelName: "gpt-4.1-2025-04-14", - matchPattern: "(?i)^(openai/)?(gpt-4.1-2025-04-14)$", - startDate: "2025-04-15T10:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - input_cached_tokens: 5e-7, - input_cached_text_tokens: 5e-7, - input_cache_read: 5e-7, - output: 0.000008, - }, - }, - ], - }, - { - modelName: "gpt-4.1-mini-2025-04-14", - matchPattern: "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$", - startDate: "2025-04-15T10:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 4e-7, - input_cached_tokens: 1e-7, - input_cached_text_tokens: 1e-7, - input_cache_read: 1e-7, - output: 0.0000016, - }, - }, - ], - }, - { - modelName: "gpt-4.1-nano-2025-04-14", - matchPattern: "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$", - startDate: "2025-04-15T10:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1e-7, - input_cached_tokens: 2.5e-8, - input_cached_text_tokens: 2.5e-8, - input_cache_read: 2.5e-8, - output: 4e-7, - }, - }, - ], - }, - { - modelName: "o3", - matchPattern: "(?i)^(openai/)?(o3)$", - startDate: "2025-04-16T23:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - input_cached_tokens: 5e-7, - input_cache_read: 5e-7, - output: 0.000008, - output_reasoning_tokens: 0.000008, - output_reasoning: 0.000008, - }, - }, - ], - }, - { - modelName: "o3-2025-04-16", - matchPattern: "(?i)^(openai/)?(o3-2025-04-16)$", - startDate: "2025-04-16T23:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - input_cached_tokens: 5e-7, - input_cache_read: 5e-7, - output: 0.000008, - output_reasoning_tokens: 0.000008, - output_reasoning: 0.000008, - }, - }, - ], - }, - { - modelName: "o4-mini", - matchPattern: "(?i)^(o4-mini)$", - startDate: "2025-04-16T23:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000011, - input_cached_tokens: 2.75e-7, - input_cache_read: 2.75e-7, - output: 0.0000044, - output_reasoning_tokens: 0.0000044, - output_reasoning: 0.0000044, - }, - }, - ], - }, - { - modelName: "o4-mini-2025-04-16", - matchPattern: "(?i)^(o4-mini-2025-04-16)$", - startDate: "2025-04-16T23:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000011, - input_cached_tokens: 2.75e-7, - input_cache_read: 2.75e-7, - output: 0.0000044, - output_reasoning_tokens: 0.0000044, - output_reasoning: 0.0000044, - }, - }, - ], - }, - { - modelName: "gemini-2.0-flash", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$", - startDate: "2025-04-22T10:11:35.241Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1e-7, - output: 4e-7, - }, - }, - ], - }, - { - modelName: "gemini-2.0-flash-lite-preview", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$", - startDate: "2025-04-22T10:11:35.241Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 7.5e-8, - output: 3e-7, - }, - }, - ], - }, - { - modelName: "gpt-4.1-nano", - matchPattern: "(?i)^(openai/)?(gpt-4.1-nano)$", - startDate: "2025-04-22T10:11:35.241Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1e-7, - input_cached_tokens: 2.5e-8, - input_cached_text_tokens: 2.5e-8, - input_cache_read: 2.5e-8, - output: 4e-7, - }, - }, - ], - }, - { - modelName: "gpt-4.1-mini", - matchPattern: "(?i)^(openai/)?(gpt-4.1-mini)$", - startDate: "2025-04-22T10:11:35.241Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 4e-7, - input_cached_tokens: 1e-7, - input_cached_text_tokens: 1e-7, - input_cache_read: 1e-7, - output: 0.0000016, - }, - }, - ], - }, - { - modelName: "claude-sonnet-4-5-20250929", - matchPattern: - "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$", - startDate: "2025-09-29T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - { - name: "Large Context", - isDefault: false, - priority: 1, - conditions: [ + "modelName": "gpt-4o", + "matchPattern": "(?i)^(openai/)?(gpt-4o)$", + "startDate": "2024-05-13T23:15:07.670Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "input_cached_tokens": 0.00000125, + "input_cache_read": 0.00000125, + "output": 0.00001 + } + } + ] + }, + { + "modelName": "gpt-4o-2024-05-13", + "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-05-13)$", + "startDate": "2024-05-13T23:15:07.670Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000005, + "output": 0.000015 + } + } + ] + }, + { + "modelName": "gpt-4-1106-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4-1106-preview)$", + "startDate": "2024-04-23T10:37:17.092Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "gpt-4-turbo-vision", + "matchPattern": "(?i)^(openai/)?(gpt-4(-\\d{4})?-vision-preview)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "gpt-4-32k", + "matchPattern": "(?i)^(openai/)?(gpt-4-32k)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00006, + "output": 0.00012 + } + } + ] + }, + { + "modelName": "gpt-4-32k-0613", + "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0613)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00006, + "output": 0.00012 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-1106", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-1106)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000001, + "output": 0.000002 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-0613", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0613)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000015, + "output": 0.000002 + } + } + ] + }, + { + "modelName": "gpt-4-0613", + "matchPattern": "(?i)^(openai/)?(gpt-4-0613)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00003, + "output": 0.00006 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-instruct", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-instruct)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000015, + "output": 0.000002 + } + } + ] + }, + { + "modelName": "text-ada-001", + "matchPattern": "(?i)^(text-ada-001)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 0.000004 + } + } + ] + }, + { + "modelName": "text-babbage-001", + "matchPattern": "(?i)^(text-babbage-001)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 5e-7 + } + } + ] + }, + { + "modelName": "text-curie-001", + "matchPattern": "(?i)^(text-curie-001)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 0.00002 + } + } + ] + }, + { + "modelName": "text-davinci-001", + "matchPattern": "(?i)^(text-davinci-001)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 0.00002 + } + } + ] + }, + { + "modelName": "text-davinci-002", + "matchPattern": "(?i)^(text-davinci-002)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 0.00002 + } + } + ] + }, + { + "modelName": "text-davinci-003", + "matchPattern": "(?i)^(text-davinci-003)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 0.00002 + } + } + ] + }, + { + "modelName": "text-embedding-ada-002-v2", + "matchPattern": "(?i)^(text-embedding-ada-002-v2)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 1e-7 + } + } + ] + }, + { + "modelName": "text-embedding-ada-002", + "matchPattern": "(?i)^(text-embedding-ada-002)$", + "startDate": "2024-01-24T18:18:50.861Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 1e-7 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-16k-0613", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k-0613)$", + "startDate": "2024-02-03T17:29:57.350Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "output": 0.000004 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-0301", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0301)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "output": 0.000002 + } + } + ] + }, + { + "modelName": "gpt-4-32k-0314", + "matchPattern": "(?i)^(openai/)?(gpt-4-32k-0314)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00006, + "output": 0.00012 + } + } + ] + }, + { + "modelName": "gpt-4-0314", + "matchPattern": "(?i)^(openai/)?(gpt-4-0314)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00003, + "output": 0.00006 + } + } + ] + }, + { + "modelName": "gpt-4", + "matchPattern": "(?i)^(openai/)?(gpt-4)$", + "startDate": "2024-01-24T10:19:21.693Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00003, + "output": 0.00006 + } + } + ] + }, + { + "modelName": "claude-instant-1.2", + "matchPattern": "(?i)^(anthropic/)?(claude-instant-1.2)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000163, + "output": 0.00000551 + } + } + ] + }, + { + "modelName": "claude-2.0", + "matchPattern": "(?i)^(anthropic/)?(claude-2.0)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000008, + "output": 0.000024 + } + } + ] + }, + { + "modelName": "claude-2.1", + "matchPattern": "(?i)^(anthropic/)?(claude-2.1)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000008, + "output": 0.000024 + } + } + ] + }, + { + "modelName": "claude-1.3", + "matchPattern": "(?i)^(anthropic/)?(claude-1.3)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000008, + "output": 0.000024 + } + } + ] + }, + { + "modelName": "claude-1.2", + "matchPattern": "(?i)^(anthropic/)?(claude-1.2)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000008, + "output": 0.000024 + } + } + ] + }, + { + "modelName": "claude-1.1", + "matchPattern": "(?i)^(anthropic/)?(claude-1.1)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000008, + "output": 0.000024 + } + } + ] + }, + { + "modelName": "claude-instant-1", + "matchPattern": "(?i)^(anthropic/)?(claude-instant-1)$", + "startDate": "2024-01-30T15:44:13.447Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000163, + "output": 0.00000551 + } + } + ] + }, + { + "modelName": "babbage-002", + "matchPattern": "(?i)^(babbage-002)$", + "startDate": "2024-01-26T17:35:21.129Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 4e-7, + "output": 0.0000016 + } + } + ] + }, + { + "modelName": "davinci-002", + "matchPattern": "(?i)^(davinci-002)$", + "startDate": "2024-01-26T17:35:21.129Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000006, + "output": 0.000012 + } + } + ] + }, + { + "modelName": "text-embedding-3-small", + "matchPattern": "(?i)^(text-embedding-3-small)$", + "startDate": "2024-01-26T17:35:21.129Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 2e-8 + } + } + ] + }, + { + "modelName": "text-embedding-3-large", + "matchPattern": "(?i)^(text-embedding-3-large)$", + "startDate": "2024-01-26T17:35:21.129Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 1.3e-7 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-0125", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-0125)$", + "startDate": "2024-01-26T17:35:21.129Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 5e-7, + "output": 0.0000015 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo)$", + "startDate": "2024-02-13T12:00:37.424Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 5e-7, + "output": 0.0000015 + } + } + ] + }, + { + "modelName": "gpt-4-0125-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4-0125-preview)$", + "startDate": "2024-01-26T17:35:21.129Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "ft:gpt-3.5-turbo-1106", + "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "output": 0.000006 + } + } + ] + }, + { + "modelName": "ft:gpt-3.5-turbo-0613", + "matchPattern": "(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000012, + "output": 0.000016 + } + } + ] + }, + { + "modelName": "ft:davinci-002", + "matchPattern": "(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000012, + "output": 0.000012 + } + } + ] + }, + { + "modelName": "ft:babbage-002", + "matchPattern": "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000016, + "output": 0.0000016 + } + } + ] + }, + { + "modelName": "chat-bison", + "matchPattern": "(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "codechat-bison-32k", + "matchPattern": "(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "codechat-bison", + "matchPattern": "(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "text-bison-32k", + "matchPattern": "(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "chat-bison-32k", + "matchPattern": "(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "text-unicorn", + "matchPattern": "(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "output": 0.0000075 + } + } + ] + }, + { + "modelName": "text-bison", + "matchPattern": "(?i)^(text-bison)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "textembedding-gecko", + "matchPattern": "(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 1e-7 + } + } + ] + }, + { + "modelName": "textembedding-gecko-multilingual", + "matchPattern": "(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "total": 1e-7 + } + } + ] + }, + { + "modelName": "code-gecko", + "matchPattern": "(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "code-bison", + "matchPattern": "(?i)^(code-bison)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "code-bison-32k", + "matchPattern": "(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-01-31T13:25:02.141Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "gpt-3.5-turbo-16k", + "matchPattern": "(?i)^(openai/)?(gpt-)(35|3.5)(-turbo-16k)$", + "startDate": "2024-02-13T12:00:37.424Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 5e-7, + "output": 0.0000015 + } + } + ] + }, + { + "modelName": "gpt-4-turbo-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-preview)$", + "startDate": "2024-02-15T21:21:50.947Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "claude-3-opus-20240229", + "matchPattern": "(?i)^(anthropic/)?(claude-3-opus-20240229|anthropic\\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$", + "startDate": "2024-03-07T17:55:38.139Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "output": 0.000075 + } + } + ] + }, + { + "modelName": "claude-3-sonnet-20240229", + "matchPattern": "(?i)^(anthropic/)?(claude-3-sonnet-20240229|anthropic\\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$", + "startDate": "2024-03-07T17:55:38.139Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "claude-3-haiku-20240307", + "matchPattern": "(?i)^(anthropic/)?(claude-3-haiku-20240307|anthropic\\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$", + "startDate": "2024-03-14T09:41:18.736Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 0.00000125 + } + } + ] + }, + { + "modelName": "gemini-1.0-pro-latest", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro-latest)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-04-11T10:27:46.517Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "output": 5e-7 + } + } + ] + }, + { + "modelName": "gemini-1.0-pro", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-04-11T10:27:46.517Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1.25e-7, + "output": 3.75e-7 + } + } + ] + }, + { + "modelName": "gemini-1.0-pro-001", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.0-pro-001)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-04-11T10:27:46.517Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1.25e-7, + "output": 3.75e-7 + } + } + ] + }, + { + "modelName": "gemini-pro", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-pro)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-04-11T10:27:46.517Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1.25e-7, + "output": 3.75e-7 + } + } + ] + }, + { + "modelName": "gemini-1.5-pro-latest", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-1.5-pro-latest)(@[a-zA-Z0-9]+)?$", + "startDate": "2024-04-11T10:27:46.517Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "output": 0.0000075 + } + } + ] + }, + { + "modelName": "gpt-4-turbo-2024-04-09", + "matchPattern": "(?i)^(openai/)?(gpt-4-turbo-2024-04-09)$", + "startDate": "2024-04-23T10:37:17.092Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "gpt-4-turbo", + "matchPattern": "(?i)^(openai/)?(gpt-4-turbo)$", + "startDate": "2024-04-11T21:13:44.989Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "gpt-4-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4-preview)$", + "startDate": "2024-04-23T10:37:17.092Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00001, + "output": 0.00003 + } + } + ] + }, + { + "modelName": "claude-3-5-sonnet-20240620", + "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20240620|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$", + "startDate": "2024-06-25T11:47:24.475Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "gpt-4o-mini", + "matchPattern": "(?i)^(openai/)?(gpt-4o-mini)$", + "startDate": "2024-07-18T17:56:09.591Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1.5e-7, + "output": 6e-7, + "input_cached_tokens": 7.5e-8, + "input_cache_read": 7.5e-8 + } + } + ] + }, + { + "modelName": "gpt-4o-mini-2024-07-18", + "matchPattern": "(?i)^(openai/)?(gpt-4o-mini-2024-07-18)$", + "startDate": "2024-07-18T17:56:09.591Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1.5e-7, + "input_cached_tokens": 7.5e-8, + "input_cache_read": 7.5e-8, + "output": 6e-7 + } + } + ] + }, + { + "modelName": "gpt-4o-2024-08-06", + "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-08-06)$", + "startDate": "2024-08-07T11:54:31.298Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "input_cached_tokens": 0.00000125, + "input_cache_read": 0.00000125, + "output": 0.00001 + } + } + ] + }, + { + "modelName": "o1-preview", + "matchPattern": "(?i)^(openai/)?(o1-preview)$", + "startDate": "2024-09-13T10:01:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "input_cached_tokens": 0.0000075, + "input_cache_read": 0.0000075, + "output": 0.00006, + "output_reasoning_tokens": 0.00006, + "output_reasoning": 0.00006 + } + } + ] + }, + { + "modelName": "o1-preview-2024-09-12", + "matchPattern": "(?i)^(openai/)?(o1-preview-2024-09-12)$", + "startDate": "2024-09-13T10:01:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "input_cached_tokens": 0.0000075, + "input_cache_read": 0.0000075, + "output": 0.00006, + "output_reasoning_tokens": 0.00006, + "output_reasoning": 0.00006 + } + } + ] + }, + { + "modelName": "o1-mini", + "matchPattern": "(?i)^(openai/)?(o1-mini)$", + "startDate": "2024-09-13T10:01:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000011, + "input_cached_tokens": 5.5e-7, + "input_cache_read": 5.5e-7, + "output": 0.0000044, + "output_reasoning_tokens": 0.0000044, + "output_reasoning": 0.0000044 + } + } + ] + }, + { + "modelName": "o1-mini-2024-09-12", + "matchPattern": "(?i)^(openai/)?(o1-mini-2024-09-12)$", + "startDate": "2024-09-13T10:01:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000011, + "input_cached_tokens": 5.5e-7, + "input_cache_read": 5.5e-7, + "output": 0.0000044, + "output_reasoning_tokens": 0.0000044, + "output_reasoning": 0.0000044 + } + } + ] + }, + { + "modelName": "claude-3.5-sonnet-20241022", + "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-sonnet-20241022-v2:0|claude-3-5-sonnet-V2@20241022)$", + "startDate": "2024-10-22T18:48:01.676Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "claude-3.5-sonnet-latest", + "matchPattern": "(?i)^(anthropic/)?(claude-3-5-sonnet-latest)$", + "startDate": "2024-10-22T18:48:01.676Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "claude-3-5-haiku-20241022", + "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-20241022|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3-5-haiku-20241022-v1:0|claude-3-5-haiku-V1@20241022)$", + "startDate": "2024-11-05T10:30:50.566Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 8e-7, + "input_tokens": 8e-7, + "output": 0.000004, + "output_tokens": 0.000004, + "cache_creation_input_tokens": 0.000001, + "input_cache_creation": 0.000001, + "input_cache_creation_5m": 0.000001, + "input_cache_creation_1h": 0.0000016, + "cache_read_input_tokens": 8e-8, + "input_cache_read": 8e-8 + } + } + ] + }, + { + "modelName": "claude-3.5-haiku-latest", + "matchPattern": "(?i)^(anthropic/)?(claude-3-5-haiku-latest)$", + "startDate": "2024-11-05T10:30:50.566Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 8e-7, + "input_tokens": 8e-7, + "output": 0.000004, + "output_tokens": 0.000004, + "cache_creation_input_tokens": 0.000001, + "input_cache_creation": 0.000001, + "input_cache_creation_5m": 0.000001, + "input_cache_creation_1h": 0.0000016, + "cache_read_input_tokens": 8e-8, + "input_cache_read": 8e-8 + } + } + ] + }, + { + "modelName": "chatgpt-4o-latest", + "matchPattern": "(?i)^(chatgpt-4o-latest)$", + "startDate": "2024-11-25T12:47:17.504Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000005, + "output": 0.000015 + } + } + ] + }, + { + "modelName": "gpt-4o-2024-11-20", + "matchPattern": "(?i)^(openai/)?(gpt-4o-2024-11-20)$", + "startDate": "2024-12-03T10:06:12.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "input_cached_tokens": 0.00000125, + "input_cache_read": 0.00000125, + "output": 0.00001 + } + } + ] + }, + { + "modelName": "gpt-4o-audio-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview)$", + "startDate": "2024-12-03T10:19:56.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input_text_tokens": 0.0000025, + "output_text_tokens": 0.00001, + "input_audio_tokens": 0.0001, + "input_audio": 0.0001, + "output_audio_tokens": 0.0002, + "output_audio": 0.0002 + } + } + ] + }, + { + "modelName": "gpt-4o-audio-preview-2024-10-01", + "matchPattern": "(?i)^(openai/)?(gpt-4o-audio-preview-2024-10-01)$", + "startDate": "2024-12-03T10:19:56.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input_text_tokens": 0.0000025, + "output_text_tokens": 0.00001, + "input_audio_tokens": 0.0001, + "input_audio": 0.0001, + "output_audio_tokens": 0.0002, + "output_audio": 0.0002 + } + } + ] + }, + { + "modelName": "gpt-4o-realtime-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview)$", + "startDate": "2024-12-03T10:19:56.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input_text_tokens": 0.000005, + "input_cached_text_tokens": 0.0000025, + "output_text_tokens": 0.00002, + "input_audio_tokens": 0.0001, + "input_audio": 0.0001, + "input_cached_audio_tokens": 0.00002, + "output_audio_tokens": 0.0002, + "output_audio": 0.0002 + } + } + ] + }, + { + "modelName": "gpt-4o-realtime-preview-2024-10-01", + "matchPattern": "(?i)^(openai/)?(gpt-4o-realtime-preview-2024-10-01)$", + "startDate": "2024-12-03T10:19:56.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input_text_tokens": 0.000005, + "input_cached_text_tokens": 0.0000025, + "output_text_tokens": 0.00002, + "input_audio_tokens": 0.0001, + "input_audio": 0.0001, + "input_cached_audio_tokens": 0.00002, + "output_audio_tokens": 0.0002, + "output_audio": 0.0002 + } + } + ] + }, + { + "modelName": "o1", + "matchPattern": "(?i)^(openai/)?(o1)$", + "startDate": "2025-01-17T00:01:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "input_cached_tokens": 0.0000075, + "input_cache_read": 0.0000075, + "output": 0.00006, + "output_reasoning_tokens": 0.00006, + "output_reasoning": 0.00006 + } + } + ] + }, + { + "modelName": "o1-2024-12-17", + "matchPattern": "(?i)^(openai/)?(o1-2024-12-17)$", + "startDate": "2025-01-17T00:01:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "input_cached_tokens": 0.0000075, + "input_cache_read": 0.0000075, + "output": 0.00006, + "output_reasoning_tokens": 0.00006, + "output_reasoning": 0.00006 + } + } + ] + }, + { + "modelName": "o3-mini", + "matchPattern": "(?i)^(openai/)?(o3-mini)$", + "startDate": "2025-01-31T20:41:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000011, + "input_cached_tokens": 5.5e-7, + "input_cache_read": 5.5e-7, + "output": 0.0000044, + "output_reasoning_tokens": 0.0000044, + "output_reasoning": 0.0000044 + } + } + ] + }, + { + "modelName": "o3-mini-2025-01-31", + "matchPattern": "(?i)^(openai/)?(o3-mini-2025-01-31)$", + "startDate": "2025-01-31T20:41:35.373Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000011, + "input_cached_tokens": 5.5e-7, + "input_cache_read": 5.5e-7, + "output": 0.0000044, + "output_reasoning_tokens": 0.0000044, + "output_reasoning": 0.0000044 + } + } + ] + }, + { + "modelName": "gemini-2.0-flash-001", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-001)(@[a-zA-Z0-9]+)?$", + "startDate": "2025-02-06T11:11:35.241Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1e-7, + "output": 4e-7 + } + } + ] + }, + { + "modelName": "gemini-2.0-flash-lite-preview-02-05", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview-02-05)(@[a-zA-Z0-9]+)?$", + "startDate": "2025-02-06T11:11:35.241Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 7.5e-8, + "output": 3e-7 + } + } + ] + }, + { + "modelName": "claude-3.7-sonnet-20250219", + "matchPattern": "(?i)^(anthropic/)?(claude-3.7-sonnet-20250219|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$", + "startDate": "2025-02-25T09:35:39.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "claude-3.7-sonnet-latest", + "matchPattern": "(?i)^(anthropic/)?(claude-3-7-sonnet-latest)$", + "startDate": "2025-02-25T09:35:39.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "gpt-4.5-preview", + "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview)$", + "startDate": "2025-02-27T21:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000075, + "input_cached_tokens": 0.0000375, + "input_cached_text_tokens": 0.0000375, + "input_cache_read": 0.0000375, + "output": 0.00015 + } + } + ] + }, + { + "modelName": "gpt-4.5-preview-2025-02-27", + "matchPattern": "(?i)^(openai/)?(gpt-4.5-preview-2025-02-27)$", + "startDate": "2025-02-27T21:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000075, + "input_cached_tokens": 0.0000375, + "input_cached_text_tokens": 0.0000375, + "input_cache_read": 0.0000375, + "output": 0.00015 + } + } + ] + }, + { + "modelName": "gpt-4.1", + "matchPattern": "(?i)^(openai/)?(gpt-4.1)$", + "startDate": "2025-04-15T10:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "input_cached_tokens": 5e-7, + "input_cached_text_tokens": 5e-7, + "input_cache_read": 5e-7, + "output": 0.000008 + } + } + ] + }, + { + "modelName": "gpt-4.1-2025-04-14", + "matchPattern": "(?i)^(openai/)?(gpt-4.1-2025-04-14)$", + "startDate": "2025-04-15T10:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "input_cached_tokens": 5e-7, + "input_cached_text_tokens": 5e-7, + "input_cache_read": 5e-7, + "output": 0.000008 + } + } + ] + }, + { + "modelName": "gpt-4.1-mini-2025-04-14", + "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini-2025-04-14)$", + "startDate": "2025-04-15T10:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 4e-7, + "input_cached_tokens": 1e-7, + "input_cached_text_tokens": 1e-7, + "input_cache_read": 1e-7, + "output": 0.0000016 + } + } + ] + }, + { + "modelName": "gpt-4.1-nano-2025-04-14", + "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano-2025-04-14)$", + "startDate": "2025-04-15T10:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1e-7, + "input_cached_tokens": 2.5e-8, + "input_cached_text_tokens": 2.5e-8, + "input_cache_read": 2.5e-8, + "output": 4e-7 + } + } + ] + }, + { + "modelName": "o3", + "matchPattern": "(?i)^(openai/)?(o3)$", + "startDate": "2025-04-16T23:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "input_cached_tokens": 5e-7, + "input_cache_read": 5e-7, + "output": 0.000008, + "output_reasoning_tokens": 0.000008, + "output_reasoning": 0.000008 + } + } + ] + }, + { + "modelName": "o3-2025-04-16", + "matchPattern": "(?i)^(openai/)?(o3-2025-04-16)$", + "startDate": "2025-04-16T23:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "input_cached_tokens": 5e-7, + "input_cache_read": 5e-7, + "output": 0.000008, + "output_reasoning_tokens": 0.000008, + "output_reasoning": 0.000008 + } + } + ] + }, + { + "modelName": "o4-mini", + "matchPattern": "(?i)^(o4-mini)$", + "startDate": "2025-04-16T23:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000011, + "input_cached_tokens": 2.75e-7, + "input_cache_read": 2.75e-7, + "output": 0.0000044, + "output_reasoning_tokens": 0.0000044, + "output_reasoning": 0.0000044 + } + } + ] + }, + { + "modelName": "o4-mini-2025-04-16", + "matchPattern": "(?i)^(o4-mini-2025-04-16)$", + "startDate": "2025-04-16T23:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000011, + "input_cached_tokens": 2.75e-7, + "input_cache_read": 2.75e-7, + "output": 0.0000044, + "output_reasoning_tokens": 0.0000044, + "output_reasoning": 0.0000044 + } + } + ] + }, + { + "modelName": "gemini-2.0-flash", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash)(@[a-zA-Z0-9]+)?$", + "startDate": "2025-04-22T10:11:35.241Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1e-7, + "output": 4e-7 + } + } + ] + }, + { + "modelName": "gemini-2.0-flash-lite-preview", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.0-flash-lite-preview)(@[a-zA-Z0-9]+)?$", + "startDate": "2025-04-22T10:11:35.241Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 7.5e-8, + "output": 3e-7 + } + } + ] + }, + { + "modelName": "gpt-4.1-nano", + "matchPattern": "(?i)^(openai/)?(gpt-4.1-nano)$", + "startDate": "2025-04-22T10:11:35.241Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1e-7, + "input_cached_tokens": 2.5e-8, + "input_cached_text_tokens": 2.5e-8, + "input_cache_read": 2.5e-8, + "output": 4e-7 + } + } + ] + }, + { + "modelName": "gpt-4.1-mini", + "matchPattern": "(?i)^(openai/)?(gpt-4.1-mini)$", + "startDate": "2025-04-22T10:11:35.241Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 4e-7, + "input_cached_tokens": 1e-7, + "input_cached_text_tokens": 1e-7, + "input_cache_read": 1e-7, + "output": 0.0000016 + } + } + ] + }, + { + "modelName": "claude-sonnet-4-5-20250929", + "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-5(-20250929)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-5(-20250929)?-v1(:0)?|claude-sonnet-4-5-V1(@20250929)?|claude-sonnet-4-5(@20250929)?)$", + "startDate": "2025-09-29T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + }, + { + "name": "Large Context", + "isDefault": false, + "priority": 1, + "conditions": [ { - usageDetailPattern: "input", - operator: "gt", - value: 200000, - }, + "usageDetailPattern": "input", + "operator": "gt", + "value": 200000 + } ], - prices: { - input: 0.000006, - input_tokens: 0.000006, - output: 0.0000225, - output_tokens: 0.0000225, - cache_creation_input_tokens: 0.0000075, - input_cache_creation: 0.0000075, - input_cache_creation_5m: 0.0000075, - input_cache_creation_1h: 0.000012, - cache_read_input_tokens: 6e-7, - input_cache_read: 6e-7, - }, - }, - ], - }, - { - modelName: "claude-sonnet-4-20250514", - matchPattern: - "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$", - startDate: "2025-05-22T17:09:02.131Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-sonnet-4-latest", - matchPattern: "(?i)^(anthropic/)?(claude-sonnet-4-latest)$", - startDate: "2025-05-22T17:09:02.131Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - ], - }, - { - modelName: "claude-opus-4-20250514", - matchPattern: - "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$", - startDate: "2025-05-22T17:09:02.131Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - input_tokens: 0.000015, - output: 0.000075, - output_tokens: 0.000075, - cache_creation_input_tokens: 0.00001875, - input_cache_creation: 0.00001875, - input_cache_creation_5m: 0.00001875, - input_cache_creation_1h: 0.00003, - cache_read_input_tokens: 0.0000015, - input_cache_read: 0.0000015, - }, - }, - ], - }, - { - modelName: "o3-pro", - matchPattern: "(?i)^(openai/)?(o3-pro)$", - startDate: "2025-06-10T22:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00002, - output: 0.00008, - output_reasoning_tokens: 0.00008, - output_reasoning: 0.00008, - }, - }, - ], - }, - { - modelName: "o3-pro-2025-06-10", - matchPattern: "(?i)^(openai/)?(o3-pro-2025-06-10)$", - startDate: "2025-06-10T22:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00002, - output: 0.00008, - output_reasoning_tokens: 0.00008, - output_reasoning: 0.00008, - }, - }, - ], - }, - { - modelName: "o1-pro", - matchPattern: "(?i)^(openai/)?(o1-pro)$", - startDate: "2025-06-10T22:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00015, - output: 0.0006, - output_reasoning_tokens: 0.0006, - output_reasoning: 0.0006, - }, - }, - ], - }, - { - modelName: "o1-pro-2025-03-19", - matchPattern: "(?i)^(openai/)?(o1-pro-2025-03-19)$", - startDate: "2025-06-10T22:26:54.132Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00015, - output: 0.0006, - output_reasoning_tokens: 0.0006, - output_reasoning: 0.0006, - }, - }, - ], - }, - { - modelName: "gemini-2.5-flash", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-flash)$", - startDate: "2025-07-03T13:44:06.964Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 3e-7, - input_text: 3e-7, - input_modality_1: 3e-7, - prompt_token_count: 3e-7, - promptTokenCount: 3e-7, - input_cached_tokens: 3e-8, - cached_content_token_count: 3e-8, - output: 0.0000025, - output_text: 0.0000025, - output_modality_1: 0.0000025, - candidates_token_count: 0.0000025, - candidatesTokenCount: 0.0000025, - thoughtsTokenCount: 0.0000025, - thoughts_token_count: 0.0000025, - output_reasoning: 0.0000025, - input_audio_tokens: 0.000001, - }, - }, - ], - }, - { - modelName: "gemini-2.5-flash-lite", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-flash-lite)$", - startDate: "2025-07-03T13:44:06.964Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 1e-7, - input_text: 1e-7, - input_modality_1: 1e-7, - prompt_token_count: 1e-7, - promptTokenCount: 1e-7, - input_cached_tokens: 2.5e-8, - cached_content_token_count: 2.5e-8, - output: 4e-7, - output_text: 4e-7, - output_modality_1: 4e-7, - candidates_token_count: 4e-7, - candidatesTokenCount: 4e-7, - thoughtsTokenCount: 4e-7, - thoughts_token_count: 4e-7, - output_reasoning: 4e-7, - input_audio_tokens: 5e-7, - }, - }, - ], - }, - { - modelName: "claude-opus-4-1-20250805", - matchPattern: - "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$", - startDate: "2025-08-05T15:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - input_tokens: 0.000015, - output: 0.000075, - output_tokens: 0.000075, - cache_creation_input_tokens: 0.00001875, - input_cache_creation: 0.00001875, - input_cache_creation_5m: 0.00001875, - input_cache_creation_1h: 0.00003, - cache_read_input_tokens: 0.0000015, - input_cache_read: 0.0000015, - }, - }, - ], - }, - { - modelName: "gpt-5", - matchPattern: "(?i)^(openai/)?(gpt-5)$", - startDate: "2025-08-07T16:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000125, - input_cached_tokens: 1.25e-7, - output: 0.00001, - input_cache_read: 1.25e-7, - output_reasoning_tokens: 0.00001, - output_reasoning: 0.00001, - }, - }, - ], - }, - { - modelName: "gpt-5-2025-08-07", - matchPattern: "(?i)^(openai/)?(gpt-5-2025-08-07)$", - startDate: "2025-08-11T08:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000125, - input_cached_tokens: 1.25e-7, - output: 0.00001, - input_cache_read: 1.25e-7, - output_reasoning_tokens: 0.00001, - output_reasoning: 0.00001, - }, - }, - ], - }, - { - modelName: "gpt-5-mini", - matchPattern: "(?i)^(openai/)?(gpt-5-mini)$", - startDate: "2025-08-07T16:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - input_cached_tokens: 2.5e-8, - output: 0.000002, - input_cache_read: 2.5e-8, - output_reasoning_tokens: 0.000002, - output_reasoning: 0.000002, - }, - }, - ], - }, - { - modelName: "gpt-5-mini-2025-08-07", - matchPattern: "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$", - startDate: "2025-08-11T08:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - input_cached_tokens: 2.5e-8, - output: 0.000002, - input_cache_read: 2.5e-8, - output_reasoning_tokens: 0.000002, - output_reasoning: 0.000002, - }, - }, - ], - }, - { - modelName: "gpt-5-nano", - matchPattern: "(?i)^(openai/)?(gpt-5-nano)$", - startDate: "2025-08-07T16:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 5e-8, - input_cached_tokens: 5e-9, - output: 4e-7, - input_cache_read: 5e-9, - output_reasoning_tokens: 4e-7, - output_reasoning: 4e-7, - }, - }, - ], - }, - { - modelName: "gpt-5-nano-2025-08-07", - matchPattern: "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$", - startDate: "2025-08-11T08:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 5e-8, - input_cached_tokens: 5e-9, - output: 4e-7, - input_cache_read: 5e-9, - output_reasoning_tokens: 4e-7, - output_reasoning: 4e-7, - }, - }, - ], - }, - { - modelName: "gpt-5-chat-latest", - matchPattern: "(?i)^(openai/)?(gpt-5-chat-latest)$", - startDate: "2025-08-07T16:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000125, - input_cached_tokens: 1.25e-7, - output: 0.00001, - input_cache_read: 1.25e-7, - output_reasoning_tokens: 0.00001, - output_reasoning: 0.00001, - }, - }, - ], - }, - { - modelName: "gpt-5-pro", - matchPattern: "(?i)^(openai/)?(gpt-5-pro)$", - startDate: "2025-10-07T08:03:54.727Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - output: 0.00012, - output_reasoning_tokens: 0.00012, - output_reasoning: 0.00012, - }, - }, - ], - }, - { - modelName: "gpt-5-pro-2025-10-06", - matchPattern: "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$", - startDate: "2025-10-07T08:03:54.727Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000015, - output: 0.00012, - output_reasoning_tokens: 0.00012, - output_reasoning: 0.00012, - }, - }, - ], - }, - { - modelName: "claude-haiku-4-5-20251001", - matchPattern: - "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$", - startDate: "2025-10-16T08:20:44.558Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000001, - input_tokens: 0.000001, - output: 0.000005, - output_tokens: 0.000005, - cache_creation_input_tokens: 0.00000125, - input_cache_creation: 0.00000125, - input_cache_creation_5m: 0.00000125, - input_cache_creation_1h: 0.000002, - cache_read_input_tokens: 1e-7, - input_cache_read: 1e-7, - }, - }, - ], - }, - { - modelName: "gpt-5.1", - matchPattern: "(?i)^(openai/)?(gpt-5.1)$", - startDate: "2025-11-14T08:57:23.481Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000125, - input_cached_tokens: 1.25e-7, - output: 0.00001, - input_cache_read: 1.25e-7, - output_reasoning_tokens: 0.00001, - output_reasoning: 0.00001, - }, - }, - ], - }, - { - modelName: "gpt-5.1-2025-11-13", - matchPattern: "(?i)^(openai/)?(gpt-5.1-2025-11-13)$", - startDate: "2025-11-14T08:57:23.481Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000125, - input_cached_tokens: 1.25e-7, - output: 0.00001, - input_cache_read: 1.25e-7, - output_reasoning_tokens: 0.00001, - output_reasoning: 0.00001, - }, - }, - ], - }, - { - modelName: "claude-opus-4-5-20251101", - matchPattern: - "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$", - startDate: "2025-11-24T20:53:27.571Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000005, - input_tokens: 0.000005, - output: 0.000025, - output_tokens: 0.000025, - cache_creation_input_tokens: 0.00000625, - input_cache_creation: 0.00000625, - input_cache_creation_5m: 0.00000625, - input_cache_creation_1h: 0.00001, - cache_read_input_tokens: 5e-7, - input_cache_read: 5e-7, - }, - }, - ], - }, - { - modelName: "claude-sonnet-4-6", - matchPattern: - "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$", - startDate: "2026-02-18T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000003, - input_tokens: 0.000003, - output: 0.000015, - output_tokens: 0.000015, - cache_creation_input_tokens: 0.00000375, - input_cache_creation: 0.00000375, - input_cache_creation_5m: 0.00000375, - input_cache_creation_1h: 0.000006, - cache_read_input_tokens: 3e-7, - input_cache_read: 3e-7, - }, - }, - { - name: "Large Context", - isDefault: false, - priority: 1, - conditions: [ + "prices": { + "input": 0.000006, + "input_tokens": 0.000006, + "output": 0.0000225, + "output_tokens": 0.0000225, + "cache_creation_input_tokens": 0.0000075, + "input_cache_creation": 0.0000075, + "input_cache_creation_5m": 0.0000075, + "input_cache_creation_1h": 0.000012, + "cache_read_input_tokens": 6e-7, + "input_cache_read": 6e-7 + } + } + ] + }, + { + "modelName": "claude-sonnet-4-20250514", + "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4(-20250514)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4(-20250514)?-v1(:0)?|claude-sonnet-4-V1(@20250514)?|claude-sonnet-4(@20250514)?)$", + "startDate": "2025-05-22T17:09:02.131Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "claude-sonnet-4-latest", + "matchPattern": "(?i)^(anthropic/)?(claude-sonnet-4-latest)$", + "startDate": "2025-05-22T17:09:02.131Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + } + ] + }, + { + "modelName": "claude-opus-4-20250514", + "matchPattern": "(?i)^(anthropic/)?(claude-opus-4(-20250514)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4(-20250514)?-v1(:0)?|claude-opus-4(@20250514)?)$", + "startDate": "2025-05-22T17:09:02.131Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "input_tokens": 0.000015, + "output": 0.000075, + "output_tokens": 0.000075, + "cache_creation_input_tokens": 0.00001875, + "input_cache_creation": 0.00001875, + "input_cache_creation_5m": 0.00001875, + "input_cache_creation_1h": 0.00003, + "cache_read_input_tokens": 0.0000015, + "input_cache_read": 0.0000015 + } + } + ] + }, + { + "modelName": "o3-pro", + "matchPattern": "(?i)^(openai/)?(o3-pro)$", + "startDate": "2025-06-10T22:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00002, + "output": 0.00008, + "output_reasoning_tokens": 0.00008, + "output_reasoning": 0.00008 + } + } + ] + }, + { + "modelName": "o3-pro-2025-06-10", + "matchPattern": "(?i)^(openai/)?(o3-pro-2025-06-10)$", + "startDate": "2025-06-10T22:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00002, + "output": 0.00008, + "output_reasoning_tokens": 0.00008, + "output_reasoning": 0.00008 + } + } + ] + }, + { + "modelName": "o1-pro", + "matchPattern": "(?i)^(openai/)?(o1-pro)$", + "startDate": "2025-06-10T22:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00015, + "output": 0.0006, + "output_reasoning_tokens": 0.0006, + "output_reasoning": 0.0006 + } + } + ] + }, + { + "modelName": "o1-pro-2025-03-19", + "matchPattern": "(?i)^(openai/)?(o1-pro-2025-03-19)$", + "startDate": "2025-06-10T22:26:54.132Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00015, + "output": 0.0006, + "output_reasoning_tokens": 0.0006, + "output_reasoning": 0.0006 + } + } + ] + }, + { + "modelName": "gemini-2.5-flash", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-flash)$", + "startDate": "2025-07-03T13:44:06.964Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 3e-7, + "input_text": 3e-7, + "input_modality_1": 3e-7, + "prompt_token_count": 3e-7, + "promptTokenCount": 3e-7, + "input_cached_tokens": 3e-8, + "cached_content_token_count": 3e-8, + "output": 0.0000025, + "output_text": 0.0000025, + "output_modality_1": 0.0000025, + "candidates_token_count": 0.0000025, + "candidatesTokenCount": 0.0000025, + "thoughtsTokenCount": 0.0000025, + "thoughts_token_count": 0.0000025, + "output_reasoning": 0.0000025, + "input_audio_tokens": 0.000001 + } + } + ] + }, + { + "modelName": "gemini-2.5-flash-lite", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-flash-lite)$", + "startDate": "2025-07-03T13:44:06.964Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 1e-7, + "input_text": 1e-7, + "input_modality_1": 1e-7, + "prompt_token_count": 1e-7, + "promptTokenCount": 1e-7, + "input_cached_tokens": 2.5e-8, + "cached_content_token_count": 2.5e-8, + "output": 4e-7, + "output_text": 4e-7, + "output_modality_1": 4e-7, + "candidates_token_count": 4e-7, + "candidatesTokenCount": 4e-7, + "thoughtsTokenCount": 4e-7, + "thoughts_token_count": 4e-7, + "output_reasoning": 4e-7, + "input_audio_tokens": 5e-7 + } + } + ] + }, + { + "modelName": "claude-opus-4-1-20250805", + "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-1(-20250805)?|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-1(-20250805)?-v1(:0)?|claude-opus-4-1(@20250805)?)$", + "startDate": "2025-08-05T15:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "input_tokens": 0.000015, + "output": 0.000075, + "output_tokens": 0.000075, + "cache_creation_input_tokens": 0.00001875, + "input_cache_creation": 0.00001875, + "input_cache_creation_5m": 0.00001875, + "input_cache_creation_1h": 0.00003, + "cache_read_input_tokens": 0.0000015, + "input_cache_read": 0.0000015 + } + } + ] + }, + { + "modelName": "gpt-5", + "matchPattern": "(?i)^(openai/)?(gpt-5)$", + "startDate": "2025-08-07T16:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000125, + "input_cached_tokens": 1.25e-7, + "output": 0.00001, + "input_cache_read": 1.25e-7, + "output_reasoning_tokens": 0.00001, + "output_reasoning": 0.00001 + } + } + ] + }, + { + "modelName": "gpt-5-2025-08-07", + "matchPattern": "(?i)^(openai/)?(gpt-5-2025-08-07)$", + "startDate": "2025-08-11T08:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000125, + "input_cached_tokens": 1.25e-7, + "output": 0.00001, + "input_cache_read": 1.25e-7, + "output_reasoning_tokens": 0.00001, + "output_reasoning": 0.00001 + } + } + ] + }, + { + "modelName": "gpt-5-mini", + "matchPattern": "(?i)^(openai/)?(gpt-5-mini)$", + "startDate": "2025-08-07T16:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "input_cached_tokens": 2.5e-8, + "output": 0.000002, + "input_cache_read": 2.5e-8, + "output_reasoning_tokens": 0.000002, + "output_reasoning": 0.000002 + } + } + ] + }, + { + "modelName": "gpt-5-mini-2025-08-07", + "matchPattern": "(?i)^(openai/)?(gpt-5-mini-2025-08-07)$", + "startDate": "2025-08-11T08:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "input_cached_tokens": 2.5e-8, + "output": 0.000002, + "input_cache_read": 2.5e-8, + "output_reasoning_tokens": 0.000002, + "output_reasoning": 0.000002 + } + } + ] + }, + { + "modelName": "gpt-5-nano", + "matchPattern": "(?i)^(openai/)?(gpt-5-nano)$", + "startDate": "2025-08-07T16:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 5e-8, + "input_cached_tokens": 5e-9, + "output": 4e-7, + "input_cache_read": 5e-9, + "output_reasoning_tokens": 4e-7, + "output_reasoning": 4e-7 + } + } + ] + }, + { + "modelName": "gpt-5-nano-2025-08-07", + "matchPattern": "(?i)^(openai/)?(gpt-5-nano-2025-08-07)$", + "startDate": "2025-08-11T08:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 5e-8, + "input_cached_tokens": 5e-9, + "output": 4e-7, + "input_cache_read": 5e-9, + "output_reasoning_tokens": 4e-7, + "output_reasoning": 4e-7 + } + } + ] + }, + { + "modelName": "gpt-5-chat-latest", + "matchPattern": "(?i)^(openai/)?(gpt-5-chat-latest)$", + "startDate": "2025-08-07T16:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000125, + "input_cached_tokens": 1.25e-7, + "output": 0.00001, + "input_cache_read": 1.25e-7, + "output_reasoning_tokens": 0.00001, + "output_reasoning": 0.00001 + } + } + ] + }, + { + "modelName": "gpt-5-pro", + "matchPattern": "(?i)^(openai/)?(gpt-5-pro)$", + "startDate": "2025-10-07T08:03:54.727Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "output": 0.00012, + "output_reasoning_tokens": 0.00012, + "output_reasoning": 0.00012 + } + } + ] + }, + { + "modelName": "gpt-5-pro-2025-10-06", + "matchPattern": "(?i)^(openai/)?(gpt-5-pro-2025-10-06)$", + "startDate": "2025-10-07T08:03:54.727Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000015, + "output": 0.00012, + "output_reasoning_tokens": 0.00012, + "output_reasoning": 0.00012 + } + } + ] + }, + { + "modelName": "claude-haiku-4-5-20251001", + "matchPattern": "(?i)^(anthropic/)?(claude-haiku-4-5-20251001|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-haiku-4-5-20251001-v1:0|claude-4-5-haiku@20251001)$", + "startDate": "2025-10-16T08:20:44.558Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000001, + "input_tokens": 0.000001, + "output": 0.000005, + "output_tokens": 0.000005, + "cache_creation_input_tokens": 0.00000125, + "input_cache_creation": 0.00000125, + "input_cache_creation_5m": 0.00000125, + "input_cache_creation_1h": 0.000002, + "cache_read_input_tokens": 1e-7, + "input_cache_read": 1e-7 + } + } + ] + }, + { + "modelName": "gpt-5.1", + "matchPattern": "(?i)^(openai/)?(gpt-5.1)$", + "startDate": "2025-11-14T08:57:23.481Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000125, + "input_cached_tokens": 1.25e-7, + "output": 0.00001, + "input_cache_read": 1.25e-7, + "output_reasoning_tokens": 0.00001, + "output_reasoning": 0.00001 + } + } + ] + }, + { + "modelName": "gpt-5.1-2025-11-13", + "matchPattern": "(?i)^(openai/)?(gpt-5.1-2025-11-13)$", + "startDate": "2025-11-14T08:57:23.481Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000125, + "input_cached_tokens": 1.25e-7, + "output": 0.00001, + "input_cache_read": 1.25e-7, + "output_reasoning_tokens": 0.00001, + "output_reasoning": 0.00001 + } + } + ] + }, + { + "modelName": "claude-opus-4-5-20251101", + "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-5(-20251101)?|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-5(-20251101)?-v1(:0)?|claude-opus-4-5(@20251101)?)$", + "startDate": "2025-11-24T20:53:27.571Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000005, + "input_tokens": 0.000005, + "output": 0.000025, + "output_tokens": 0.000025, + "cache_creation_input_tokens": 0.00000625, + "input_cache_creation": 0.00000625, + "input_cache_creation_5m": 0.00000625, + "input_cache_creation_1h": 0.00001, + "cache_read_input_tokens": 5e-7, + "input_cache_read": 5e-7 + } + } + ] + }, + { + "modelName": "claude-sonnet-4-6", + "matchPattern": "(?i)^(anthropic\\/)?(claude-sonnet-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-sonnet-4-6(-v1(:0)?)?|claude-sonnet-4-6)$", + "startDate": "2026-02-18T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000003, + "input_tokens": 0.000003, + "output": 0.000015, + "output_tokens": 0.000015, + "cache_creation_input_tokens": 0.00000375, + "input_cache_creation": 0.00000375, + "input_cache_creation_5m": 0.00000375, + "input_cache_creation_1h": 0.000006, + "cache_read_input_tokens": 3e-7, + "input_cache_read": 3e-7 + } + }, + { + "name": "Large Context", + "isDefault": false, + "priority": 1, + "conditions": [ { - usageDetailPattern: "input", - operator: "gt", - value: 200000, - }, + "usageDetailPattern": "input", + "operator": "gt", + "value": 200000 + } ], - prices: { - input: 0.000006, - input_tokens: 0.000006, - output: 0.0000225, - output_tokens: 0.0000225, - cache_creation_input_tokens: 0.0000075, - input_cache_creation: 0.0000075, - input_cache_creation_5m: 0.0000075, - input_cache_creation_1h: 0.000012, - cache_read_input_tokens: 6e-7, - input_cache_read: 6e-7, - }, - }, - ], - }, - { - modelName: "claude-opus-4-6", - matchPattern: - "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$", - startDate: "2026-02-09T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000005, - input_tokens: 0.000005, - output: 0.000025, - output_tokens: 0.000025, - cache_creation_input_tokens: 0.00000625, - input_cache_creation: 0.00000625, - input_cache_creation_5m: 0.00000625, - input_cache_creation_1h: 0.00001, - cache_read_input_tokens: 5e-7, - input_cache_read: 5e-7, - }, - }, - ], - }, - { - modelName: "gemini-2.5-pro", - matchPattern: "(?i)^(google(ai)?/)?(gemini-2.5-pro)$", - startDate: "2025-11-26T13:27:53.545Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000125, - input_text: 0.00000125, - input_modality_1: 0.00000125, - prompt_token_count: 0.00000125, - promptTokenCount: 0.00000125, - input_cached_tokens: 1.25e-7, - cached_content_token_count: 1.25e-7, - output: 0.00001, - output_text: 0.00001, - output_modality_1: 0.00001, - candidates_token_count: 0.00001, - candidatesTokenCount: 0.00001, - thoughtsTokenCount: 0.00001, - thoughts_token_count: 0.00001, - output_reasoning: 0.00001, - }, - }, - { - name: "Large Context", - isDefault: false, - priority: 1, - conditions: [ + "prices": { + "input": 0.000006, + "input_tokens": 0.000006, + "output": 0.0000225, + "output_tokens": 0.0000225, + "cache_creation_input_tokens": 0.0000075, + "input_cache_creation": 0.0000075, + "input_cache_creation_5m": 0.0000075, + "input_cache_creation_1h": 0.000012, + "cache_read_input_tokens": 6e-7, + "input_cache_read": 6e-7 + } + } + ] + }, + { + "modelName": "claude-opus-4-6", + "matchPattern": "(?i)^(anthropic/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.|global\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$", + "startDate": "2026-02-09T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000005, + "input_tokens": 0.000005, + "output": 0.000025, + "output_tokens": 0.000025, + "cache_creation_input_tokens": 0.00000625, + "input_cache_creation": 0.00000625, + "input_cache_creation_5m": 0.00000625, + "input_cache_creation_1h": 0.00001, + "cache_read_input_tokens": 5e-7, + "input_cache_read": 5e-7 + } + } + ] + }, + { + "modelName": "gemini-2.5-pro", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-2.5-pro)$", + "startDate": "2025-11-26T13:27:53.545Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000125, + "input_text": 0.00000125, + "input_modality_1": 0.00000125, + "prompt_token_count": 0.00000125, + "promptTokenCount": 0.00000125, + "input_cached_tokens": 1.25e-7, + "cached_content_token_count": 1.25e-7, + "output": 0.00001, + "output_text": 0.00001, + "output_modality_1": 0.00001, + "candidates_token_count": 0.00001, + "candidatesTokenCount": 0.00001, + "thoughtsTokenCount": 0.00001, + "thoughts_token_count": 0.00001, + "output_reasoning": 0.00001 + } + }, + { + "name": "Large Context", + "isDefault": false, + "priority": 1, + "conditions": [ { - usageDetailPattern: "(input|prompt|cached)", - operator: "gt", - value: 200000, - }, + "usageDetailPattern": "(input|prompt|cached)", + "operator": "gt", + "value": 200000 + } ], - prices: { - input: 0.0000025, - input_text: 0.0000025, - input_modality_1: 0.0000025, - prompt_token_count: 0.0000025, - promptTokenCount: 0.0000025, - input_cached_tokens: 2.5e-7, - cached_content_token_count: 2.5e-7, - output: 0.000015, - output_text: 0.000015, - output_modality_1: 0.000015, - candidates_token_count: 0.000015, - candidatesTokenCount: 0.000015, - thoughtsTokenCount: 0.000015, - thoughts_token_count: 0.000015, - output_reasoning: 0.000015, - }, - }, - ], - }, - { - modelName: "gemini-3-pro-preview", - matchPattern: "(?i)^(google(ai)?/)?(gemini-3-pro-preview)$", - startDate: "2025-11-26T13:27:53.545Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - input_text: 0.000002, - input_modality_1: 0.000002, - prompt_token_count: 0.000002, - promptTokenCount: 0.000002, - input_cached_tokens: 2e-7, - cached_content_token_count: 2e-7, - output: 0.000012, - output_text: 0.000012, - output_modality_1: 0.000012, - candidates_token_count: 0.000012, - candidatesTokenCount: 0.000012, - thoughtsTokenCount: 0.000012, - thoughts_token_count: 0.000012, - output_reasoning: 0.000012, - }, - }, - { - name: "Large Context", - isDefault: false, - priority: 1, - conditions: [ + "prices": { + "input": 0.0000025, + "input_text": 0.0000025, + "input_modality_1": 0.0000025, + "prompt_token_count": 0.0000025, + "promptTokenCount": 0.0000025, + "input_cached_tokens": 2.5e-7, + "cached_content_token_count": 2.5e-7, + "output": 0.000015, + "output_text": 0.000015, + "output_modality_1": 0.000015, + "candidates_token_count": 0.000015, + "candidatesTokenCount": 0.000015, + "thoughtsTokenCount": 0.000015, + "thoughts_token_count": 0.000015, + "output_reasoning": 0.000015 + } + } + ] + }, + { + "modelName": "gemini-3-pro-preview", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-3-pro-preview)$", + "startDate": "2025-11-26T13:27:53.545Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "input_text": 0.000002, + "input_modality_1": 0.000002, + "prompt_token_count": 0.000002, + "promptTokenCount": 0.000002, + "input_cached_tokens": 2e-7, + "cached_content_token_count": 2e-7, + "output": 0.000012, + "output_text": 0.000012, + "output_modality_1": 0.000012, + "candidates_token_count": 0.000012, + "candidatesTokenCount": 0.000012, + "thoughtsTokenCount": 0.000012, + "thoughts_token_count": 0.000012, + "output_reasoning": 0.000012 + } + }, + { + "name": "Large Context", + "isDefault": false, + "priority": 1, + "conditions": [ { - usageDetailPattern: "(input|prompt|cached)", - operator: "gt", - value: 200000, - }, + "usageDetailPattern": "(input|prompt|cached)", + "operator": "gt", + "value": 200000 + } ], - prices: { - input: 0.000004, - input_text: 0.000004, - input_modality_1: 0.000004, - prompt_token_count: 0.000004, - promptTokenCount: 0.000004, - input_cached_tokens: 4e-7, - cached_content_token_count: 4e-7, - output: 0.000018, - output_text: 0.000018, - output_modality_1: 0.000018, - candidates_token_count: 0.000018, - candidatesTokenCount: 0.000018, - thoughtsTokenCount: 0.000018, - thoughts_token_count: 0.000018, - output_reasoning: 0.000018, - }, - }, - ], - }, - { - modelName: "gemini-3.1-pro-preview", - matchPattern: "(?i)^(google(ai)?/)?(gemini-3.1-pro-preview(-customtools)?)$", - startDate: "2026-02-19T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000002, - input_modality_1: 0.000002, - input_text: 0.000002, - prompt_token_count: 0.000002, - promptTokenCount: 0.000002, - input_cached_tokens: 2e-7, - cached_content_token_count: 2e-7, - output: 0.000012, - output_text: 0.000012, - output_modality_1: 0.000012, - candidates_token_count: 0.000012, - candidatesTokenCount: 0.000012, - thoughtsTokenCount: 0.000012, - thoughts_token_count: 0.000012, - output_reasoning: 0.000012, - }, - }, - { - name: "Large Context", - isDefault: false, - priority: 1, - conditions: [ + "prices": { + "input": 0.000004, + "input_text": 0.000004, + "input_modality_1": 0.000004, + "prompt_token_count": 0.000004, + "promptTokenCount": 0.000004, + "input_cached_tokens": 4e-7, + "cached_content_token_count": 4e-7, + "output": 0.000018, + "output_text": 0.000018, + "output_modality_1": 0.000018, + "candidates_token_count": 0.000018, + "candidatesTokenCount": 0.000018, + "thoughtsTokenCount": 0.000018, + "thoughts_token_count": 0.000018, + "output_reasoning": 0.000018 + } + } + ] + }, + { + "modelName": "gemini-3.1-pro-preview", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-3.1-pro-preview(-customtools)?)$", + "startDate": "2026-02-19T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000002, + "input_modality_1": 0.000002, + "input_text": 0.000002, + "prompt_token_count": 0.000002, + "promptTokenCount": 0.000002, + "input_cached_tokens": 2e-7, + "cached_content_token_count": 2e-7, + "output": 0.000012, + "output_text": 0.000012, + "output_modality_1": 0.000012, + "candidates_token_count": 0.000012, + "candidatesTokenCount": 0.000012, + "thoughtsTokenCount": 0.000012, + "thoughts_token_count": 0.000012, + "output_reasoning": 0.000012 + } + }, + { + "name": "Large Context", + "isDefault": false, + "priority": 1, + "conditions": [ { - usageDetailPattern: "(input|prompt|cached)", - operator: "gt", - value: 200000, - }, + "usageDetailPattern": "(input|prompt|cached)", + "operator": "gt", + "value": 200000 + } ], - prices: { - input: 0.000004, - input_modality_1: 0.000004, - input_text: 0.000004, - prompt_token_count: 0.000004, - promptTokenCount: 0.000004, - input_cached_tokens: 4e-7, - cached_content_token_count: 4e-7, - output: 0.000018, - output_text: 0.000018, - output_modality_1: 0.000018, - candidates_token_count: 0.000018, - candidatesTokenCount: 0.000018, - thoughtsTokenCount: 0.000018, - thoughts_token_count: 0.000018, - output_reasoning: 0.000018, - }, - }, - ], - }, - { - modelName: "gpt-5.2", - matchPattern: "(?i)^(openai/)?(gpt-5.2)$", - startDate: "2025-12-12T09:00:06.513Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000175, - input_cached_tokens: 1.75e-7, - input_cache_read: 1.75e-7, - output: 0.000014, - output_reasoning_tokens: 0.000014, - output_reasoning: 0.000014, - }, - }, - ], - }, - { - modelName: "gpt-5.2-2025-12-11", - matchPattern: "(?i)^(openai/)?(gpt-5.2-2025-12-11)$", - startDate: "2025-12-12T09:00:06.513Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00000175, - input_cached_tokens: 1.75e-7, - input_cache_read: 1.75e-7, - output: 0.000014, - output_reasoning_tokens: 0.000014, - output_reasoning: 0.000014, - }, - }, - ], - }, - { - modelName: "gpt-5.2-pro", - matchPattern: "(?i)^(openai/)?(gpt-5.2-pro)$", - startDate: "2025-12-12T09:00:06.513Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000021, - output: 0.000168, - output_reasoning_tokens: 0.000168, - output_reasoning: 0.000168, - }, - }, - ], - }, - { - modelName: "gpt-5.2-pro-2025-12-11", - matchPattern: "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$", - startDate: "2025-12-12T09:00:06.513Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.000021, - output: 0.000168, - output_reasoning_tokens: 0.000168, - output_reasoning: 0.000168, - }, - }, - ], - }, - { - modelName: "gpt-5.4", - matchPattern: "(?i)^(openai/)?(gpt-5.4)$", - startDate: "2026-03-05T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - input_cached_tokens: 2.5e-7, - input_cache_read: 2.5e-7, - output: 0.000015, - output_reasoning_tokens: 0.000015, - output_reasoning: 0.000015, - }, - }, - ], - }, - { - modelName: "gpt-5.4-pro", - matchPattern: "(?i)^(openai/)?(gpt-5.4-pro)$", - startDate: "2026-03-05T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00003, - output: 0.00018, - output_reasoning_tokens: 0.00018, - output_reasoning: 0.00018, - }, - }, - ], - }, - { - modelName: "gpt-5.4-2026-03-05", - matchPattern: "(?i)^(openai/)?(gpt-5.4-2026-03-05)$", - startDate: "2026-03-05T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.0000025, - input_cached_tokens: 2.5e-7, - input_cache_read: 2.5e-7, - output: 0.000015, - output_reasoning_tokens: 0.000015, - output_reasoning: 0.000015, - }, - }, - ], - }, - { - modelName: "gpt-5.4-pro-2026-03-05", - matchPattern: "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$", - startDate: "2026-03-05T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 0.00003, - output: 0.00018, - output_reasoning_tokens: 0.00018, - output_reasoning: 0.00018, - }, - }, - ], - }, - { - modelName: "gpt-5.4-mini", - matchPattern: "(?i)^(openai\\/)?(gpt-5.4-mini)$", - startDate: "2026-03-18T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 7.5e-7, - input_cached_tokens: 7.5e-8, - input_cache_read: 7.5e-8, - output: 0.0000045, - }, - }, - ], - }, - { - modelName: "gpt-5.4-mini-2026-03-17", - matchPattern: "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", - startDate: "2026-03-18T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 7.5e-7, - input_cached_tokens: 7.5e-8, - input_cache_read: 7.5e-8, - output: 0.0000045, - }, - }, - ], - }, - { - modelName: "gpt-5.4-nano", - matchPattern: "(?i)^(openai\\/)?(gpt-5.4-nano)$", - startDate: "2026-03-18T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2e-7, - input_cached_tokens: 2e-8, - input_cache_read: 2e-8, - output: 0.00000125, - }, - }, - ], - }, - { - modelName: "gpt-5.4-nano-2026-03-17", - matchPattern: "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", - startDate: "2026-03-18T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2e-7, - input_cached_tokens: 2e-8, - input_cache_read: 2e-8, - output: 0.00000125, - }, - }, - ], - }, - { - modelName: "gemini-3-flash-preview", - matchPattern: "(?i)^(google(ai)?/)?(gemini-3-flash-preview)$", - startDate: "2025-12-21T12:01:42.282Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 5e-7, - input_text: 5e-7, - input_modality_1: 5e-7, - prompt_token_count: 5e-7, - promptTokenCount: 5e-7, - input_cached_tokens: 5e-8, - cached_content_token_count: 5e-8, - output: 0.000003, - output_text: 0.000003, - output_modality_1: 0.000003, - candidates_token_count: 0.000003, - candidatesTokenCount: 0.000003, - thoughtsTokenCount: 0.000003, - thoughts_token_count: 0.000003, - output_reasoning: 0.000003, - }, - }, - ], - }, - { - modelName: "gemini-3.1-flash-lite-preview", - matchPattern: "(?i)^(google(ai)?/)?(gemini-3.1-flash-lite-preview)$", - startDate: "2026-03-03T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input: 2.5e-7, - input_modality_1: 2.5e-7, - input_text: 2.5e-7, - prompt_token_count: 2.5e-7, - promptTokenCount: 2.5e-7, - input_cached_tokens: 2.5e-8, - cached_content_token_count: 2.5e-8, - output: 0.0000015, - output_text: 0.0000015, - output_modality_1: 0.0000015, - candidates_token_count: 0.0000015, - candidatesTokenCount: 0.0000015, - thoughtsTokenCount: 0.0000015, - thoughts_token_count: 0.0000015, - output_reasoning: 0.0000015, - input_audio_tokens: 5e-7, - }, - }, - ], - }, - { - modelName: "gemini-live-2.5-flash-native-audio", - matchPattern: "(?i)^(google/)?(gemini-live-2.5-flash-native-audio)$", - startDate: "2026-03-16T00:00:00.000Z", - pricingTiers: [ - { - name: "Standard", - isDefault: true, - priority: 0, - conditions: [], - prices: { - input_text: 5e-7, - input_audio: 0.000003, - input_image: 0.000003, - output_text: 0.000002, - output_audio: 0.000012, - }, - }, - ], - }, + "prices": { + "input": 0.000004, + "input_modality_1": 0.000004, + "input_text": 0.000004, + "prompt_token_count": 0.000004, + "promptTokenCount": 0.000004, + "input_cached_tokens": 4e-7, + "cached_content_token_count": 4e-7, + "output": 0.000018, + "output_text": 0.000018, + "output_modality_1": 0.000018, + "candidates_token_count": 0.000018, + "candidatesTokenCount": 0.000018, + "thoughtsTokenCount": 0.000018, + "thoughts_token_count": 0.000018, + "output_reasoning": 0.000018 + } + } + ] + }, + { + "modelName": "gpt-5.2", + "matchPattern": "(?i)^(openai/)?(gpt-5.2)$", + "startDate": "2025-12-12T09:00:06.513Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000175, + "input_cached_tokens": 1.75e-7, + "input_cache_read": 1.75e-7, + "output": 0.000014, + "output_reasoning_tokens": 0.000014, + "output_reasoning": 0.000014 + } + } + ] + }, + { + "modelName": "gpt-5.2-2025-12-11", + "matchPattern": "(?i)^(openai/)?(gpt-5.2-2025-12-11)$", + "startDate": "2025-12-12T09:00:06.513Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00000175, + "input_cached_tokens": 1.75e-7, + "input_cache_read": 1.75e-7, + "output": 0.000014, + "output_reasoning_tokens": 0.000014, + "output_reasoning": 0.000014 + } + } + ] + }, + { + "modelName": "gpt-5.2-pro", + "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro)$", + "startDate": "2025-12-12T09:00:06.513Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000021, + "output": 0.000168, + "output_reasoning_tokens": 0.000168, + "output_reasoning": 0.000168 + } + } + ] + }, + { + "modelName": "gpt-5.2-pro-2025-12-11", + "matchPattern": "(?i)^(openai/)?(gpt-5.2-pro-2025-12-11)$", + "startDate": "2025-12-12T09:00:06.513Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.000021, + "output": 0.000168, + "output_reasoning_tokens": 0.000168, + "output_reasoning": 0.000168 + } + } + ] + }, + { + "modelName": "gpt-5.4", + "matchPattern": "(?i)^(openai/)?(gpt-5.4)$", + "startDate": "2026-03-05T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "input_cached_tokens": 2.5e-7, + "input_cache_read": 2.5e-7, + "output": 0.000015, + "output_reasoning_tokens": 0.000015, + "output_reasoning": 0.000015 + } + } + ] + }, + { + "modelName": "gpt-5.4-pro", + "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro)$", + "startDate": "2026-03-05T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00003, + "output": 0.00018, + "output_reasoning_tokens": 0.00018, + "output_reasoning": 0.00018 + } + } + ] + }, + { + "modelName": "gpt-5.4-2026-03-05", + "matchPattern": "(?i)^(openai/)?(gpt-5.4-2026-03-05)$", + "startDate": "2026-03-05T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.0000025, + "input_cached_tokens": 2.5e-7, + "input_cache_read": 2.5e-7, + "output": 0.000015, + "output_reasoning_tokens": 0.000015, + "output_reasoning": 0.000015 + } + } + ] + }, + { + "modelName": "gpt-5.4-pro-2026-03-05", + "matchPattern": "(?i)^(openai/)?(gpt-5.4-pro-2026-03-05)$", + "startDate": "2026-03-05T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 0.00003, + "output": 0.00018, + "output_reasoning_tokens": 0.00018, + "output_reasoning": 0.00018 + } + } + ] + }, + { + "modelName": "gpt-5.4-mini", + "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini)$", + "startDate": "2026-03-18T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 7.5e-7, + "input_cached_tokens": 7.5e-8, + "input_cache_read": 7.5e-8, + "output": 0.0000045 + } + } + ] + }, + { + "modelName": "gpt-5.4-mini-2026-03-17", + "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-mini-2026-03-17)$", + "startDate": "2026-03-18T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 7.5e-7, + "input_cached_tokens": 7.5e-8, + "input_cache_read": 7.5e-8, + "output": 0.0000045 + } + } + ] + }, + { + "modelName": "gpt-5.4-nano", + "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano)$", + "startDate": "2026-03-18T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2e-7, + "input_cached_tokens": 2e-8, + "input_cache_read": 2e-8, + "output": 0.00000125 + } + } + ] + }, + { + "modelName": "gpt-5.4-nano-2026-03-17", + "matchPattern": "(?i)^(openai\\/)?(gpt-5.4-nano-2026-03-17)$", + "startDate": "2026-03-18T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2e-7, + "input_cached_tokens": 2e-8, + "input_cache_read": 2e-8, + "output": 0.00000125 + } + } + ] + }, + { + "modelName": "gemini-3-flash-preview", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-3-flash-preview)$", + "startDate": "2025-12-21T12:01:42.282Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 5e-7, + "input_text": 5e-7, + "input_modality_1": 5e-7, + "prompt_token_count": 5e-7, + "promptTokenCount": 5e-7, + "input_cached_tokens": 5e-8, + "cached_content_token_count": 5e-8, + "output": 0.000003, + "output_text": 0.000003, + "output_modality_1": 0.000003, + "candidates_token_count": 0.000003, + "candidatesTokenCount": 0.000003, + "thoughtsTokenCount": 0.000003, + "thoughts_token_count": 0.000003, + "output_reasoning": 0.000003 + } + } + ] + }, + { + "modelName": "gemini-3.1-flash-lite-preview", + "matchPattern": "(?i)^(google(ai)?/)?(gemini-3.1-flash-lite-preview)$", + "startDate": "2026-03-03T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input": 2.5e-7, + "input_modality_1": 2.5e-7, + "input_text": 2.5e-7, + "prompt_token_count": 2.5e-7, + "promptTokenCount": 2.5e-7, + "input_cached_tokens": 2.5e-8, + "cached_content_token_count": 2.5e-8, + "output": 0.0000015, + "output_text": 0.0000015, + "output_modality_1": 0.0000015, + "candidates_token_count": 0.0000015, + "candidatesTokenCount": 0.0000015, + "thoughtsTokenCount": 0.0000015, + "thoughts_token_count": 0.0000015, + "output_reasoning": 0.0000015, + "input_audio_tokens": 5e-7 + } + } + ] + }, + { + "modelName": "gemini-live-2.5-flash-native-audio", + "matchPattern": "(?i)^(google/)?(gemini-live-2.5-flash-native-audio)$", + "startDate": "2026-03-16T00:00:00.000Z", + "pricingTiers": [ + { + "name": "Standard", + "isDefault": true, + "priority": 0, + "conditions": [], + "prices": { + "input_text": 5e-7, + "input_audio": 0.000003, + "input_image": 0.000003, + "output_text": 0.000002, + "output_audio": 0.000012 + } + } + ] + } ]; diff --git a/internal-packages/llm-model-catalog/src/modelCatalog.ts b/internal-packages/llm-model-catalog/src/modelCatalog.ts index d90eb1a992b..71ae921c3e7 100644 --- a/internal-packages/llm-model-catalog/src/modelCatalog.ts +++ b/internal-packages/llm-model-catalog/src/modelCatalog.ts @@ -5,564 +5,677 @@ import type { ModelCatalogEntry } from "./types.js"; export const modelCatalog: Record = { "chatgpt-4o-latest": { - provider: "openai", - description: - "OpenAI's flagship multimodal model optimized for speed and cost, capable of processing text, images, and audio with strong performance across reasoning, coding, and creative tasks.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship multimodal model optimized for speed and cost, capable of processing text, images, and audio with strong performance across reasoning, coding, and creative tasks.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable", + "fine_tunable" ], - releaseDate: "2024-05-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T10:55:46.469Z", - baseModelName: "chatgpt-4o", + "releaseDate": "2024-05-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T10:55:46.469Z", + "baseModelName": "chatgpt-4o" }, "claude-1.1": { - provider: "anthropic", - description: - "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", - contextWindow: 9000, - maxOutputTokens: 8191, - capabilities: ["streaming"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: null, - resolvedAt: "2026-03-24T10:55:47.906Z", - baseModelName: null, + "provider": "anthropic", + "description": "An early-generation Claude model from Anthropic, offering basic conversational and text completion capabilities. It was quickly superseded by Claude 1.2, 1.3, and the Claude 2 family.", + "contextWindow": 9000, + "maxOutputTokens": 8191, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": null, + "resolvedAt": "2026-03-24T10:55:47.906Z", + "baseModelName": null }, "claude-1.2": { - provider: "anthropic", - description: - "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", - contextWindow: 9000, - maxOutputTokens: 8191, - capabilities: ["streaming"], - releaseDate: null, - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: null, - resolvedAt: "2026-03-24T10:55:46.760Z", - baseModelName: null, + "provider": "anthropic", + "description": "An early-generation Anthropic model, part of the original Claude 1.x family. It offered improved performance over Claude 1.0 but was quickly superseded by Claude 1.3 and later model families.", + "contextWindow": 9000, + "maxOutputTokens": 8191, + "capabilities": [ + "streaming" + ], + "releaseDate": null, + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": null, + "resolvedAt": "2026-03-24T10:55:46.760Z", + "baseModelName": null }, "claude-1.3": { - provider: "anthropic", - description: - "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", - contextWindow: 100000, - maxOutputTokens: null, - capabilities: ["streaming"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: null, - resolvedAt: "2026-03-24T10:55:46.227Z", - baseModelName: null, + "provider": "anthropic", + "description": "Early-generation Claude model from Anthropic, offering improved performance over Claude 1.0-1.2 in reasoning and instruction-following tasks.", + "contextWindow": 100000, + "maxOutputTokens": null, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": null, + "resolvedAt": "2026-03-24T10:55:46.227Z", + "baseModelName": null }, "claude-2.0": { - provider: "anthropic", - description: - "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", - contextWindow: 100000, - maxOutputTokens: 4096, - capabilities: ["streaming"], - releaseDate: "2023-07-11", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2023-02-01", - resolvedAt: "2026-03-24T10:55:45.922Z", - baseModelName: null, + "provider": "anthropic", + "description": "Anthropic's second-generation large language model, offering improved performance over Claude 1.x with longer context support. Succeeded by Claude 2.1 and later the Claude 3 family.", + "contextWindow": 100000, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-07-11", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2023-02-01", + "resolvedAt": "2026-03-24T10:55:45.922Z", + "baseModelName": null }, "claude-2.1": { - provider: "anthropic", - description: - "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", - contextWindow: 200000, - maxOutputTokens: 4096, - capabilities: ["streaming", "tool_use"], - releaseDate: "2023-11-21", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2023-01-01", - resolvedAt: "2026-03-24T10:56:22.743Z", - baseModelName: null, + "provider": "anthropic", + "description": "Anthropic's Claude 2.1 model featuring a 200K context window, reduced hallucination rates compared to Claude 2.0, and improved accuracy on long document comprehension.", + "contextWindow": 200000, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming", + "tool_use" + ], + "releaseDate": "2023-11-21", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2023-01-01", + "resolvedAt": "2026-03-24T10:56:22.743Z", + "baseModelName": null }, "claude-3-5-haiku-20241022": { - provider: "anthropic", - description: - "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", - contextWindow: 200000, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-10-22", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-07-01", - resolvedAt: "2026-03-24T10:56:25.724Z", - baseModelName: "claude-3-5-haiku", + "provider": "anthropic", + "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across common tasks.", + "contextWindow": 200000, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-10-22", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-07-01", + "resolvedAt": "2026-03-24T10:56:25.724Z", + "baseModelName": "claude-3-5-haiku" }, "claude-3-5-sonnet-20240620": { - provider: "anthropic", - description: - "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", - contextWindow: 200000, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-06-20", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-04-01", - resolvedAt: "2026-03-24T10:56:35.401Z", - baseModelName: "claude-3-5-sonnet", + "provider": "anthropic", + "description": "Anthropic's Claude 3.5 Sonnet is a mid-tier model balancing intelligence and speed, excelling at coding, analysis, and vision tasks while being faster and cheaper than Opus.", + "contextWindow": 200000, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-06-20", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-04-01", + "resolvedAt": "2026-03-24T10:56:35.401Z", + "baseModelName": "claude-3-5-sonnet" }, "claude-3-haiku-20240307": { - provider: "anthropic", - description: - "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", - contextWindow: 200000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-03-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-08-01", - resolvedAt: "2026-03-24T10:56:25.288Z", - baseModelName: "claude-3-haiku", + "provider": "anthropic", + "description": "Anthropic's fastest and most compact Claude 3 model, optimized for speed and cost-efficiency while maintaining strong performance on everyday tasks.", + "contextWindow": 200000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-03-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-08-01", + "resolvedAt": "2026-03-24T10:56:25.288Z", + "baseModelName": "claude-3-haiku" }, "claude-3-opus-20240229": { - provider: "anthropic", - description: - "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", - contextWindow: 200000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-03-04", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-08-01", - resolvedAt: "2026-03-24T10:56:26.008Z", - baseModelName: "claude-3-opus", + "provider": "anthropic", + "description": "Anthropic's most capable model in the Claude 3 family, excelling at complex analysis, nuanced content generation, and advanced reasoning tasks.", + "contextWindow": 200000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-03-04", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-08-01", + "resolvedAt": "2026-03-24T10:56:26.008Z", + "baseModelName": "claude-3-opus" }, "claude-3-sonnet-20240229": { - provider: "anthropic", - description: - "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", - contextWindow: 200000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-03-04", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-02-01", - resolvedAt: "2026-03-24T10:56:59.532Z", - baseModelName: "claude-3-sonnet", + "provider": "anthropic", + "description": "Mid-tier model in Anthropic's Claude 3 family, balancing performance and speed for a wide range of tasks including analysis, coding, and content generation.", + "contextWindow": 200000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-03-04", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-02-01", + "resolvedAt": "2026-03-24T10:56:59.532Z", + "baseModelName": "claude-3-sonnet" }, "claude-3.5-haiku-latest": { - provider: "anthropic", - description: - "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", - contextWindow: 200000, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-10-29", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-07-01", - resolvedAt: "2026-03-24T10:57:04.392Z", - baseModelName: "claude-3.5-haiku", + "provider": "anthropic", + "description": "Anthropic's fastest and most cost-effective model in the Claude 3.5 family, optimized for speed and efficiency while maintaining strong performance across a wide range of tasks.", + "contextWindow": 200000, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-10-29", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-07-01", + "resolvedAt": "2026-03-24T10:57:04.392Z", + "baseModelName": "claude-3.5-haiku" }, "claude-3.5-sonnet-20241022": { - provider: "anthropic", - description: - "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", - contextWindow: 200000, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-06-20", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-04-01", - resolvedAt: "2026-03-24T10:57:13.346Z", - baseModelName: "claude-3.5-sonnet", + "provider": "anthropic", + "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", + "contextWindow": 200000, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-06-20", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-04-01", + "resolvedAt": "2026-03-24T10:57:13.346Z", + "baseModelName": "claude-3.5-sonnet" }, "claude-3.5-sonnet-latest": { - provider: "anthropic", - description: - "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", - contextWindow: 200000, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-06-20", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-04-01", - resolvedAt: "2026-03-24T10:57:13.346Z", - baseModelName: "claude-3.5-sonnet", + "provider": "anthropic", + "description": "Anthropic's mid-tier model offering strong reasoning, coding, and analysis capabilities at a balance of speed and intelligence, positioned between Haiku and Opus in the Claude 3.5 family.", + "contextWindow": 200000, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-06-20", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-04-01", + "resolvedAt": "2026-03-24T10:57:13.346Z", + "baseModelName": "claude-3.5-sonnet" }, "claude-3.7-sonnet-20250219": { - provider: "anthropic", - description: - "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", - contextWindow: 200000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-02-24", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-04-01", - resolvedAt: "2026-03-24T10:57:12.967Z", - baseModelName: "claude-3.7-sonnet", + "provider": "anthropic", + "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", + "contextWindow": 200000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-02-24", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-04-01", + "resolvedAt": "2026-03-24T10:57:12.967Z", + "baseModelName": "claude-3.7-sonnet" }, "claude-3.7-sonnet-latest": { - provider: "anthropic", - description: - "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", - contextWindow: 200000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-02-24", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-04-01", - resolvedAt: "2026-03-24T10:57:12.967Z", - baseModelName: "claude-3.7-sonnet", + "provider": "anthropic", + "description": "Anthropic's Claude 3.7 Sonnet is a hybrid reasoning model that introduced extended thinking capabilities, offering strong performance on coding, math, and complex reasoning tasks.", + "contextWindow": 200000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-02-24", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-04-01", + "resolvedAt": "2026-03-24T10:57:12.967Z", + "baseModelName": "claude-3.7-sonnet" }, "claude-haiku-4-5-20251001": { - provider: "anthropic", - description: - "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", - contextWindow: 200000, - maxOutputTokens: 64000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-10-01", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-07-01", - resolvedAt: "2026-03-24T10:57:29.685Z", - baseModelName: "claude-haiku-4-5", + "provider": "anthropic", + "description": "Anthropic's fastest model with near-frontier intelligence, optimized for speed and cost efficiency while supporting extended thinking and vision.", + "contextWindow": 200000, + "maxOutputTokens": 64000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-10-01", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-07-01", + "resolvedAt": "2026-03-24T10:57:29.685Z", + "baseModelName": "claude-haiku-4-5" }, "claude-instant-1": { - provider: "anthropic", - description: - "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", - contextWindow: 100000, - maxOutputTokens: 8191, - capabilities: ["streaming"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-01-06", - knowledgeCutoff: "2023-01-01", - resolvedAt: "2026-03-24T10:57:36.888Z", - baseModelName: null, + "provider": "anthropic", + "description": "Anthropic's fast and cost-effective model optimized for speed and efficiency, positioned as a lighter alternative to Claude 1.x for tasks requiring lower latency.", + "contextWindow": 100000, + "maxOutputTokens": 8191, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-01-06", + "knowledgeCutoff": "2023-01-01", + "resolvedAt": "2026-03-24T10:57:36.888Z", + "baseModelName": null }, "claude-instant-1.2": { - provider: "anthropic", - description: - "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", - contextWindow: 100000, - maxOutputTokens: 8191, - capabilities: ["streaming"], - releaseDate: "2023-08-09", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2023-01-01", - resolvedAt: "2026-03-24T10:57:41.865Z", - baseModelName: null, + "provider": "anthropic", + "description": "Anthropic's fast and cost-effective model, optimized for speed and efficiency while maintaining strong performance on conversational and text generation tasks.", + "contextWindow": 100000, + "maxOutputTokens": 8191, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-08-09", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2023-01-01", + "resolvedAt": "2026-03-24T10:57:41.865Z", + "baseModelName": null }, "claude-opus-4-1-20250805": { - provider: "anthropic", - description: - "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", - contextWindow: 200000, - maxOutputTokens: 32000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-08-05", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-03-01", - resolvedAt: "2026-03-24T10:58:36.876Z", - baseModelName: "claude-opus-4-1", + "provider": "anthropic", + "description": "Anthropic's hybrid reasoning model with strong software engineering and agentic capabilities, scoring 74.5% on SWE-bench Verified. Supports both rapid responses and step-by-step extended thinking.", + "contextWindow": 200000, + "maxOutputTokens": 32000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-08-05", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-03-01", + "resolvedAt": "2026-03-24T10:58:36.876Z", + "baseModelName": "claude-opus-4-1" }, "claude-opus-4-20250514": { - provider: "anthropic", - description: - "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", - contextWindow: 200000, - maxOutputTokens: 32000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-05-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-03-01", - resolvedAt: "2026-03-24T10:58:47.518Z", - baseModelName: "claude-opus-4", + "provider": "anthropic", + "description": "Anthropic's flagship model from the Claude 4 family, excelling at complex coding tasks, long-running agent workflows, and deep reasoning with extended thinking support.", + "contextWindow": 200000, + "maxOutputTokens": 32000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-05-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-03-01", + "resolvedAt": "2026-03-24T10:58:47.518Z", + "baseModelName": "claude-opus-4" }, "claude-opus-4-5-20251101": { - provider: "anthropic", - description: - "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", - contextWindow: 200000, - maxOutputTokens: 64000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-11-01", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-03-01", - resolvedAt: "2026-03-24T10:58:48.961Z", - baseModelName: "claude-opus-4-5", + "provider": "anthropic", + "description": "Anthropic's flagship intelligence model released in November 2025, excelling at complex reasoning, vision, and extended thinking with the best performance in Anthropic's lineup before Opus 4.6.", + "contextWindow": 200000, + "maxOutputTokens": 64000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-11-01", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-03-01", + "resolvedAt": "2026-03-24T10:58:48.961Z", + "baseModelName": "claude-opus-4-5" }, "claude-opus-4-6": { - provider: "anthropic", - description: - "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", - contextWindow: 1000000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2026-02-05", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-05-01", - resolvedAt: "2026-03-24T10:58:42.061Z", - baseModelName: null, + "provider": "anthropic", + "description": "Anthropic's most intelligent model, optimized for building agents and coding with exceptional reasoning capabilities and extended agentic task horizons.", + "contextWindow": 1000000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2026-02-05", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-05-01", + "resolvedAt": "2026-03-24T10:58:42.061Z", + "baseModelName": null }, "claude-sonnet-4-20250514": { - provider: "anthropic", - description: - "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", - contextWindow: 200000, - maxOutputTokens: 64000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-05-14", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-03-01", - resolvedAt: "2026-03-24T10:58:39.601Z", - baseModelName: "claude-sonnet-4", + "provider": "anthropic", + "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", + "contextWindow": 200000, + "maxOutputTokens": 64000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-05-14", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-03-01", + "resolvedAt": "2026-03-24T10:58:39.601Z", + "baseModelName": "claude-sonnet-4" }, "claude-sonnet-4-5-20250929": { - provider: "anthropic", - description: - "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", - contextWindow: 200000, - maxOutputTokens: 64000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-09-29", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T10:59:54.426Z", - baseModelName: "claude-sonnet-4-5", + "provider": "anthropic", + "description": "Anthropic's high-performance mid-tier model with strong coding, reasoning, and multi-step problem solving capabilities. Successor to Claude Sonnet 4, offering improved benchmarks at the same price point.", + "contextWindow": 200000, + "maxOutputTokens": 64000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-09-29", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T10:59:54.426Z", + "baseModelName": "claude-sonnet-4-5" }, "claude-sonnet-4-6": { - provider: "anthropic", - description: - "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", - contextWindow: 1000000, - maxOutputTokens: 64000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2026-02-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2026-01-01", - resolvedAt: "2026-03-24T10:59:59.014Z", - baseModelName: null, + "provider": "anthropic", + "description": "Anthropic's best combination of speed and intelligence, excelling at coding, agentic tasks, and computer use, with a 1M token context window and performance rivaling prior Opus-class models.", + "contextWindow": 1000000, + "maxOutputTokens": 64000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2026-02-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2026-01-01", + "resolvedAt": "2026-03-24T10:59:59.014Z", + "baseModelName": null }, "claude-sonnet-4-latest": { - provider: "anthropic", - description: - "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", - contextWindow: 200000, - maxOutputTokens: 64000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-05-14", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-03-01", - resolvedAt: "2026-03-24T10:58:39.601Z", - baseModelName: "claude-sonnet-4", + "provider": "anthropic", + "description": "Anthropic's balanced Claude 4 model offering strong coding, reasoning, and multilingual performance at moderate cost. Now a legacy model superseded by Claude Sonnet 4.5 and 4.6.", + "contextWindow": 200000, + "maxOutputTokens": 64000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-05-14", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-03-01", + "resolvedAt": "2026-03-24T10:58:39.601Z", + "baseModelName": "claude-sonnet-4" }, "gemini-1.0-pro": { - provider: "google", - description: - "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", - contextWindow: 32760, - maxOutputTokens: 8192, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2023-12-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-02-15", - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T10:59:26.767Z", - baseModelName: null, + "provider": "google", + "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", + "contextWindow": 32760, + "maxOutputTokens": 8192, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-12-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-02-15", + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T10:59:26.767Z", + "baseModelName": null }, "gemini-1.0-pro-001": { - provider: "google", - description: - "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", - contextWindow: 30720, - maxOutputTokens: 2048, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2024-02-15", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-02-15", - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T10:59:27.391Z", - baseModelName: null, + "provider": "google", + "description": "Google's first-generation Pro model optimized for text generation, reasoning, and multi-turn conversation tasks, part of the original Gemini 1.0 lineup.", + "contextWindow": 30720, + "maxOutputTokens": 2048, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-02-15", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-02-15", + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T10:59:27.391Z", + "baseModelName": null }, "gemini-1.0-pro-latest": { - provider: "google", - description: - "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", - contextWindow: 32760, - maxOutputTokens: 8192, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2023-12-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-02-15", - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T10:59:26.767Z", - baseModelName: "gemini-1.0-pro", + "provider": "google", + "description": "Google's first-generation Gemini Pro model, a mid-size multimodal model designed for text generation, reasoning, and chat applications. Succeeded by Gemini 1.5 Pro.", + "contextWindow": 32760, + "maxOutputTokens": 8192, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-12-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-02-15", + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T10:59:26.767Z", + "baseModelName": "gemini-1.0-pro" }, "gemini-1.5-pro-latest": { - provider: "google", - description: - "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", - contextWindow: 2097152, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "audio_input"], - releaseDate: "2024-02-15", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-09-24", - knowledgeCutoff: "2024-04-01", - resolvedAt: "2026-03-24T10:59:25.463Z", - baseModelName: "gemini-1.5-pro", + "provider": "google", + "description": "Google's mid-size multimodal model with a massive context window, strong at long-document understanding, code generation, and multi-turn conversation.", + "contextWindow": 2097152, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "audio_input" + ], + "releaseDate": "2024-02-15", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-09-24", + "knowledgeCutoff": "2024-04-01", + "resolvedAt": "2026-03-24T10:59:25.463Z", + "baseModelName": "gemini-1.5-pro" }, "gemini-2.0-flash": { - provider: "google", - description: - "Google's second-generation workhorse model optimized for speed, with native tool use, multimodal input (text, images, audio, video), and a 1M token context window.", - contextWindow: 1048576, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution", "audio_input"], - releaseDate: "2025-02-05", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-06-01", - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:01:15.429Z", - baseModelName: null, + "provider": "google", + "description": "Google's second-generation workhorse model optimized for speed, with native tool use, multimodal input (text, images, audio, video), and a 1M token context window.", + "contextWindow": 1048576, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "code_execution", + "audio_input" + ], + "releaseDate": "2025-02-05", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-06-01", + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:01:15.429Z", + "baseModelName": null }, "gemini-2.0-flash-001": { - provider: "google", - description: - "Google's fast and efficient multimodal model that outperforms Gemini 1.5 Pro on key benchmarks at twice the speed, supporting text, image, audio, and video inputs with native tool use.", - contextWindow: 1048576, - maxOutputTokens: 8192, - capabilities: [ + "provider": "google", + "description": "Google's fast and efficient multimodal model that outperforms Gemini 1.5 Pro on key benchmarks at twice the speed, supporting text, image, audio, and video inputs with native tool use.", + "contextWindow": 1048576, + "maxOutputTokens": 8192, + "capabilities": [ "vision", "tool_use", "streaming", @@ -570,1614 +683,1890 @@ export const modelCatalog: Record = { "audio_input", "image_generation", "audio_output", - "code_execution", + "code_execution" ], - releaseDate: "2025-02-05", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-06-01", - knowledgeCutoff: "2024-08-01", - resolvedAt: "2026-03-24T11:01:04.084Z", - baseModelName: null, + "releaseDate": "2025-02-05", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-06-01", + "knowledgeCutoff": "2024-08-01", + "resolvedAt": "2026-03-24T11:01:04.084Z", + "baseModelName": null }, "gemini-2.0-flash-lite-preview": { - provider: "google", - description: - "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", - contextWindow: 1048576, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-02-05", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-06-01", - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:00:56.775Z", - baseModelName: null, + "provider": "google", + "description": "A lightweight, cost-efficient variant of Gemini 2.0 Flash optimized for low latency and high throughput, supporting multimodal input with text output.", + "contextWindow": 1048576, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-02-05", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-06-01", + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:00:56.775Z", + "baseModelName": null }, "gemini-2.0-flash-lite-preview-02-05": { - provider: "google", - description: - "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", - contextWindow: 1048576, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-02-05", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-12-09", - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:01:34.165Z", - baseModelName: null, + "provider": "google", + "description": "Google's cost-optimized, low-latency model in the Gemini 2.0 family, designed for high-volume tasks like summarization, multimodal processing, and categorization.", + "contextWindow": 1048576, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-02-05", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-12-09", + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:01:34.165Z", + "baseModelName": null }, "gemini-2.5-flash": { - provider: "google", - description: - "Google's best price-performance model optimized for low-latency, high-volume tasks requiring reasoning, with built-in thinking capabilities and multimodal input support.", - contextWindow: 1048576, - maxOutputTokens: 65536, - capabilities: [ + "provider": "google", + "description": "Google's best price-performance model optimized for low-latency, high-volume tasks requiring reasoning, with built-in thinking capabilities and multimodal input support.", + "contextWindow": 1048576, + "maxOutputTokens": 65536, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2025-06-01", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:01:25.200Z", - baseModelName: null, + "releaseDate": "2025-06-01", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:01:25.200Z", + "baseModelName": null }, "gemini-2.5-flash-lite": { - provider: "google", - description: - "Google's most cost-efficient Gemini model, optimized for low-latency use cases with strong reasoning, multilingual, and long-context capabilities at minimal cost.", - contextWindow: 1048576, - maxOutputTokens: 65535, - capabilities: [ + "provider": "google", + "description": "Google's most cost-efficient Gemini model, optimized for low-latency use cases with strong reasoning, multilingual, and long-context capabilities at minimal cost.", + "contextWindow": 1048576, + "maxOutputTokens": 65535, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2025-07-22", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-07-22", - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:02:30.060Z", - baseModelName: null, + "releaseDate": "2025-07-22", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-07-22", + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:02:30.060Z", + "baseModelName": null }, "gemini-2.5-pro": { - provider: "google", - description: - "Google's most advanced reasoning model with deep thinking capabilities, excelling at complex tasks like coding, math, and multimodal understanding across text, images, audio, and video.", - contextWindow: 1048576, - maxOutputTokens: 65535, - capabilities: [ + "provider": "google", + "description": "Google's most advanced reasoning model with deep thinking capabilities, excelling at complex tasks like coding, math, and multimodal understanding across text, images, audio, and video.", + "contextWindow": 1048576, + "maxOutputTokens": 65535, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2025-03-25", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-06-17", - knowledgeCutoff: "2025-01-31", - resolvedAt: "2026-03-24T11:02:25.573Z", - baseModelName: null, + "releaseDate": "2025-03-25", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-06-17", + "knowledgeCutoff": "2025-01-31", + "resolvedAt": "2026-03-24T11:02:25.573Z", + "baseModelName": null }, "gemini-3-flash-preview": { - provider: "google", - description: - "Google's high-speed thinking model that matches Gemini 2.5 Pro performance at ~3x faster speed and lower cost, designed for agentic workflows, multi-turn chat, and coding assistance with configurable reasoning levels.", - contextWindow: 1048576, - maxOutputTokens: 65536, - capabilities: [ + "provider": "google", + "description": "Google's high-speed thinking model that matches Gemini 2.5 Pro performance at ~3x faster speed and lower cost, designed for agentic workflows, multi-turn chat, and coding assistance with configurable reasoning levels.", + "contextWindow": 1048576, + "maxOutputTokens": 65536, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2025-12-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:02:13.388Z", - baseModelName: null, + "releaseDate": "2025-12-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:02:13.388Z", + "baseModelName": null }, "gemini-3-pro-preview": { - provider: "google", - description: - "Google's flagship reasoning and multimodal model with strong coding and agentic capabilities, now deprecated in favor of Gemini 3.1 Pro.", - contextWindow: 1048576, - maxOutputTokens: 65536, - capabilities: [ + "provider": "google", + "description": "Google's flagship reasoning and multimodal model with strong coding and agentic capabilities, now deprecated in favor of Gemini 3.1 Pro.", + "contextWindow": 1048576, + "maxOutputTokens": 65536, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2025-11-01", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-03-09", - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:02:29.313Z", - baseModelName: null, + "releaseDate": "2025-11-01", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-03-09", + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:02:29.313Z", + "baseModelName": null }, "gemini-3.1-flash-lite-preview": { - provider: "google", - description: - "Google's most cost-efficient multimodal model in the Gemini 3 series, optimized for high-volume, low-latency tasks like translation, classification, and simple data extraction. Offers 2.5x faster time-to-first-token than Gemini 2.5 Flash.", - contextWindow: 1048576, - maxOutputTokens: 65536, - capabilities: [ + "provider": "google", + "description": "Google's most cost-efficient multimodal model in the Gemini 3 series, optimized for high-volume, low-latency tasks like translation, classification, and simple data extraction. Offers 2.5x faster time-to-first-token than Gemini 2.5 Flash.", + "contextWindow": 1048576, + "maxOutputTokens": 65536, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2026-03-03", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:02:29.253Z", - baseModelName: null, + "releaseDate": "2026-03-03", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:02:29.253Z", + "baseModelName": null }, "gemini-3.1-pro-preview": { - provider: "google", - description: - "Google's most advanced reasoning model in the Gemini 3.1 family, excelling at complex problem-solving across text, audio, images, video, and code with a 1M token context window and extended thinking capabilities.", - contextWindow: 1048576, - maxOutputTokens: 65536, - capabilities: [ + "provider": "google", + "description": "Google's most advanced reasoning model in the Gemini 3.1 family, excelling at complex problem-solving across text, audio, images, video, and code with a 1M token context window and extended thinking capabilities.", + "contextWindow": 1048576, + "maxOutputTokens": 65536, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "extended_thinking", "code_execution", - "audio_input", + "audio_input" ], - releaseDate: "2026-02-19", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:03:33.071Z", - baseModelName: null, + "releaseDate": "2026-02-19", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:03:33.071Z", + "baseModelName": null }, "gemini-pro": { - provider: "google", - description: - "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", - contextWindow: 32768, - maxOutputTokens: 8192, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2023-12-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: false, - deprecationDate: "2025-04-09", - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T11:03:45.401Z", - baseModelName: null, + "provider": "google", + "description": "Google's first-generation Gemini model for text generation, reasoning, and multi-turn conversation. Superseded by Gemini 1.5 Pro and later models.", + "contextWindow": 32768, + "maxOutputTokens": 8192, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-12-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-04-09", + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T11:03:45.401Z", + "baseModelName": null }, "gpt-3.5-turbo": { - provider: "openai", - description: - "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", - contextWindow: 16385, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2023-03-01", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-09-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:03:11.412Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's fast and cost-effective model optimized for chat and instruction-following tasks, now superseded by GPT-4o mini.", + "contextWindow": 16385, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2023-03-01", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-09-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:03:11.412Z", + "baseModelName": null }, "gpt-3.5-turbo-0125": { - provider: "openai", - description: - "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", - contextWindow: 16385, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2024-01-25", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-09-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:03:11.310Z", - baseModelName: null, + "provider": "openai", + "description": "A fast and cost-effective GPT-3.5 Turbo snapshot optimized for chat completions, offering improved accuracy for function calling and reduced instances of incomplete responses.", + "contextWindow": 16385, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2024-01-25", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-09-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:03:11.310Z", + "baseModelName": null }, "gpt-3.5-turbo-0301": { - provider: "openai", - description: - "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", - contextWindow: 4096, - maxOutputTokens: 4096, - capabilities: ["streaming", "fine_tunable"], - releaseDate: "2023-03-01", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2024-06-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:03:12.060Z", - baseModelName: null, + "provider": "openai", + "description": "Early snapshot of GPT-3.5 Turbo, OpenAI's first ChatGPT-optimized model for chat completions. Fast and cost-effective for simple tasks but superseded by later revisions.", + "contextWindow": 4096, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming", + "fine_tunable" + ], + "releaseDate": "2023-03-01", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2024-06-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:03:12.060Z", + "baseModelName": null }, "gpt-3.5-turbo-0613": { - provider: "openai", - description: - "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", - contextWindow: 4096, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2023-06-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2024-09-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:04:04.463Z", - baseModelName: null, + "provider": "openai", + "description": "A snapshot of GPT-3.5 Turbo from June 2023, optimized for chat and instruction-following tasks with function calling support.", + "contextWindow": 4096, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2023-06-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2024-09-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:04:04.463Z", + "baseModelName": null }, "gpt-3.5-turbo-1106": { - provider: "openai", - description: - "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", - contextWindow: 16385, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2023-11-06", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-09-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:04:23.054Z", - baseModelName: null, + "provider": "openai", + "description": "A dated snapshot of GPT-3.5 Turbo released in November 2023, offering improved instruction following, JSON mode, and parallel function calling over previous GPT-3.5 variants.", + "contextWindow": 16385, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2023-11-06", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-09-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:04:23.054Z", + "baseModelName": null }, "gpt-3.5-turbo-16k": { - provider: "openai", - description: - "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", - contextWindow: 16384, - maxOutputTokens: 4096, - capabilities: ["streaming", "json_mode", "fine_tunable", "tool_use"], - releaseDate: "2023-06-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-09-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:04:36.307Z", - baseModelName: null, + "provider": "openai", + "description": "Extended context version of GPT-3.5 Turbo with 16K token context window, offering the same capabilities as the base model but able to process longer inputs.", + "contextWindow": 16384, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming", + "json_mode", + "fine_tunable", + "tool_use" + ], + "releaseDate": "2023-06-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-09-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:04:36.307Z", + "baseModelName": null }, "gpt-3.5-turbo-16k-0613": { - provider: "openai", - description: - "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", - contextWindow: 16384, - maxOutputTokens: 4096, - capabilities: ["streaming", "json_mode", "fine_tunable"], - releaseDate: "2023-06-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2024-09-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:04:22.894Z", - baseModelName: null, + "provider": "openai", + "description": "Extended context window variant of GPT-3.5 Turbo with 16K token context, snapshot from June 2023. Optimized for chat completions with longer document processing.", + "contextWindow": 16384, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2023-06-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2024-09-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:04:22.894Z", + "baseModelName": null }, "gpt-3.5-turbo-instruct": { - provider: "openai", - description: - "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", - contextWindow: 4096, - maxOutputTokens: 4096, - capabilities: ["fine_tunable"], - releaseDate: "2023-09-19", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-01-27", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:04:22.309Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's GPT-3.5 Turbo Instruct is a completions-only model (not chat) optimized for following explicit instructions, replacing the legacy text-davinci-003 model.", + "contextWindow": 4096, + "maxOutputTokens": 4096, + "capabilities": [ + "fine_tunable" + ], + "releaseDate": "2023-09-19", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-01-27", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:04:22.309Z", + "baseModelName": null }, "gpt-4": { - provider: "openai", - description: - "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", - contextWindow: 8192, - maxOutputTokens: 8192, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-06-06", - knowledgeCutoff: "2023-12-01", - resolvedAt: "2026-03-24T11:04:36.773Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's flagship large language model that preceded GPT-4o, known for strong reasoning and instruction-following capabilities across a wide range of tasks.", + "contextWindow": 8192, + "maxOutputTokens": 8192, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-06-06", + "knowledgeCutoff": "2023-12-01", + "resolvedAt": "2026-03-24T11:04:36.773Z", + "baseModelName": null }, "gpt-4-0125-preview": { - provider: "openai", - description: - "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2024-01-25", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-12-01", - resolvedAt: "2026-03-24T11:04:54.196Z", - baseModelName: null, + "provider": "openai", + "description": "An improved GPT-4 Turbo preview model with better task completion, reduced laziness in code generation, and enhanced instruction following.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-01-25", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-12-01", + "resolvedAt": "2026-03-24T11:04:54.196Z", + "baseModelName": null }, "gpt-4-0314": { - provider: "openai", - description: - "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", - contextWindow: 8192, - maxOutputTokens: 4096, - capabilities: ["streaming"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2024-06-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:05:14.112Z", - baseModelName: null, + "provider": "openai", + "description": "Original GPT-4 snapshot from March 2023, a large multimodal model (text-only at launch) that was one of OpenAI's first GPT-4 releases. Now deprecated and replaced by newer GPT-4 variants.", + "contextWindow": 8192, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2024-06-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:05:14.112Z", + "baseModelName": null }, "gpt-4-0613": { - provider: "openai", - description: - "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", - contextWindow: 8192, - maxOutputTokens: 8192, - capabilities: ["tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2023-06-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-06-06", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:05:13.885Z", - baseModelName: null, + "provider": "openai", + "description": "A snapshot of GPT-4 from June 2023, offering strong reasoning and instruction-following capabilities. It was one of the first widely available GPT-4 variants with function calling support.", + "contextWindow": 8192, + "maxOutputTokens": 8192, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2023-06-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-06-06", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:05:13.885Z", + "baseModelName": null }, "gpt-4-1106-preview": { - provider: "openai", - description: - "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2023-11-06", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T11:05:12.960Z", - baseModelName: null, + "provider": "openai", + "description": "GPT-4 Turbo preview model with 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-11-06", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T11:05:12.960Z", + "baseModelName": null }, "gpt-4-32k": { - provider: "openai", - description: - "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", - contextWindow: 32768, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-06-06", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:05:14.584Z", - baseModelName: null, + "provider": "openai", + "description": "Extended context window variant of GPT-4 with 32,768 token capacity, offering the same capabilities as GPT-4 but able to process longer documents and conversations.", + "contextWindow": 32768, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-06-06", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:05:14.584Z", + "baseModelName": null }, "gpt-4-32k-0314": { - provider: "openai", - description: - "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", - contextWindow: 32768, - maxOutputTokens: 4096, - capabilities: ["streaming"], - releaseDate: "2023-03-14", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2024-06-13", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:05:32.044Z", - baseModelName: null, + "provider": "openai", + "description": "Extended context (32k token) variant of the original GPT-4 launch snapshot from March 2024, offering the same capabilities as gpt-4-0314 but with 4x the context window.", + "contextWindow": 32768, + "maxOutputTokens": 4096, + "capabilities": [ + "streaming" + ], + "releaseDate": "2023-03-14", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2024-06-13", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:05:32.044Z", + "baseModelName": null }, "gpt-4-32k-0613": { - provider: "openai", - description: - "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", - contextWindow: 32768, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2023-06-13", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-06-06", - knowledgeCutoff: "2021-09-01", - resolvedAt: "2026-03-24T11:05:53.070Z", - baseModelName: null, + "provider": "openai", + "description": "Extended context window variant of GPT-4 with 32,768 token context, based on the June 2023 snapshot. Offers the same capabilities as GPT-4 but with 4x the context length.", + "contextWindow": 32768, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-06-13", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-06-06", + "knowledgeCutoff": "2021-09-01", + "resolvedAt": "2026-03-24T11:05:53.070Z", + "baseModelName": null }, "gpt-4-preview": { - provider: "openai", - description: - "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["tool_use", "streaming", "json_mode"], - releaseDate: "2023-11-06", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-06-06", - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T11:06:54.248Z", - baseModelName: null, + "provider": "openai", + "description": "GPT-4 Turbo preview model with 128K context window, JSON mode, and parallel function calling. A preview release in the GPT-4 Turbo series, now deprecated in favor of newer models.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-11-06", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-06-06", + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T11:06:54.248Z", + "baseModelName": null }, "gpt-4-turbo": { - provider: "openai", - description: - "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-04-09", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-12-01", - resolvedAt: "2026-03-24T11:05:51.415Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-04-09", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-12-01", + "resolvedAt": "2026-03-24T11:05:51.415Z", + "baseModelName": null }, "gpt-4-turbo-2024-04-09": { - provider: "openai", - description: - "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-04-09", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-12-01", - resolvedAt: "2026-03-24T11:05:51.415Z", - baseModelName: "gpt-4-turbo", + "provider": "openai", + "description": "OpenAI's optimized GPT-4 variant offering faster inference and lower cost than the original GPT-4, with vision capabilities and a 128K context window.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-04-09", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-12-01", + "resolvedAt": "2026-03-24T11:05:51.415Z", + "baseModelName": "gpt-4-turbo" }, "gpt-4-turbo-preview": { - provider: "openai", - description: - "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-01-25", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-12-01", - resolvedAt: "2026-03-24T11:05:52.346Z", - baseModelName: null, + "provider": "openai", + "description": "An early preview of GPT-4 Turbo with a 128K context window, offering improved instruction following and JSON mode support at reduced cost compared to GPT-4.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-01-25", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-12-01", + "resolvedAt": "2026-03-24T11:05:52.346Z", + "baseModelName": null }, "gpt-4-turbo-vision": { - provider: "openai", - description: - "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2023-11-06", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2024-12-06", - knowledgeCutoff: "2023-04-01", - resolvedAt: "2026-03-24T11:06:38.455Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's GPT-4 Turbo model with vision capabilities, able to analyze and understand images alongside text. It was a preview model later superseded by GPT-4 Turbo (gpt-4-turbo-2024-04-09) and then GPT-4o.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2023-11-06", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2024-12-06", + "knowledgeCutoff": "2023-04-01", + "resolvedAt": "2026-03-24T11:06:38.455Z", + "baseModelName": null }, "gpt-4.1": { - provider: "openai", - description: - "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", - contextWindow: 1047576, - maxOutputTokens: 32768, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-04-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:07:00.439Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", + "contextWindow": 1047576, + "maxOutputTokens": 32768, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-04-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:07:00.439Z", + "baseModelName": null }, "gpt-4.1-2025-04-14": { - provider: "openai", - description: - "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", - contextWindow: 1047576, - maxOutputTokens: 32768, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-04-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:07:00.439Z", - baseModelName: "gpt-4.1", + "provider": "openai", + "description": "OpenAI's flagship model optimized for coding, instruction following, and tool calling with a 1M token context window. Excels at structured outputs and long-context tasks.", + "contextWindow": 1047576, + "maxOutputTokens": 32768, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-04-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:07:00.439Z", + "baseModelName": "gpt-4.1" }, "gpt-4.1-mini": { - provider: "openai", - description: - "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", - contextWindow: 1000000, - maxOutputTokens: 32768, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2025-04-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:08:14.524Z", - baseModelName: null, + "provider": "openai", + "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", + "contextWindow": 1000000, + "maxOutputTokens": 32768, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2025-04-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:08:14.524Z", + "baseModelName": null }, "gpt-4.1-mini-2025-04-14": { - provider: "openai", - description: - "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", - contextWindow: 1000000, - maxOutputTokens: 32768, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2025-04-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:08:14.524Z", - baseModelName: "gpt-4.1-mini", + "provider": "openai", + "description": "A compact, cost-efficient model in OpenAI's GPT-4.1 family that matches or exceeds GPT-4o on many benchmarks while offering nearly half the latency and significantly lower cost.", + "contextWindow": 1000000, + "maxOutputTokens": 32768, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2025-04-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:08:14.524Z", + "baseModelName": "gpt-4.1-mini" }, "gpt-4.1-nano": { - provider: "openai", - description: - "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", - contextWindow: 1047576, - maxOutputTokens: 32768, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-04-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:08:04.533Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", + "contextWindow": 1047576, + "maxOutputTokens": 32768, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-04-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:08:04.533Z", + "baseModelName": null }, "gpt-4.1-nano-2025-04-14": { - provider: "openai", - description: - "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", - contextWindow: 1047576, - maxOutputTokens: 32768, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-04-14", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:08:04.533Z", - baseModelName: "gpt-4.1-nano", + "provider": "openai", + "description": "OpenAI's fastest and most cost-effective model in the GPT-4.1 family, optimized for low-latency tasks like classification, autocompletion, and lightweight agentic workflows with strong instruction-following and tool-calling capabilities.", + "contextWindow": 1047576, + "maxOutputTokens": 32768, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-04-14", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:08:04.533Z", + "baseModelName": "gpt-4.1-nano" }, "gpt-4.5-preview": { - provider: "openai", - description: - "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-02-27", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-07-14", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:07:57.880Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-02-27", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-07-14", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:07:57.880Z", + "baseModelName": null }, "gpt-4.5-preview-2025-02-27": { - provider: "openai", - description: - "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-02-27", - isHidden: true, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2025-07-14", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:07:57.880Z", - baseModelName: "gpt-4.5-preview", + "provider": "openai", + "description": "OpenAI's largest pretrained model before the GPT-5 series, emphasizing broad knowledge, creative writing, and improved emotional intelligence over reasoning-focused models.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-02-27", + "isHidden": true, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2025-07-14", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:07:57.880Z", + "baseModelName": "gpt-4.5-preview" }, "gpt-4o": { - provider: "openai", - description: - "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable", + "fine_tunable" ], - releaseDate: "2024-05-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:07:31.638Z", - baseModelName: null, + "releaseDate": "2024-05-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:07:31.638Z", + "baseModelName": null }, "gpt-4o-2024-05-13": { - provider: "openai", - description: - "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable", + "fine_tunable" ], - releaseDate: "2024-05-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:07:31.638Z", - baseModelName: "gpt-4o", + "releaseDate": "2024-05-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:07:31.638Z", + "baseModelName": "gpt-4o" }, "gpt-4o-2024-08-06": { - provider: "openai", - description: - "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable", + "fine_tunable" ], - releaseDate: "2024-05-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:07:31.638Z", - baseModelName: "gpt-4o", + "releaseDate": "2024-05-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:07:31.638Z", + "baseModelName": "gpt-4o" }, "gpt-4o-2024-11-20": { - provider: "openai", - description: - "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship multimodal model combining strong reasoning with vision, audio, and tool use capabilities at faster speeds and lower cost than GPT-4 Turbo.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "audio_input", "audio_output", - "fine_tunable", + "fine_tunable" ], - releaseDate: "2024-05-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:07:31.638Z", - baseModelName: "gpt-4o", + "releaseDate": "2024-05-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:07:31.638Z", + "baseModelName": "gpt-4o" }, "gpt-4o-audio-preview": { - provider: "openai", - description: - "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], - releaseDate: "2024-10-01", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-05-07", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:08:09.590Z", - baseModelName: null, + "provider": "openai", + "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "audio_input", + "audio_output", + "tool_use", + "streaming" + ], + "releaseDate": "2024-10-01", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-05-07", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:08:09.590Z", + "baseModelName": null }, "gpt-4o-audio-preview-2024-10-01": { - provider: "openai", - description: - "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], - releaseDate: "2024-10-01", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: "2026-05-07", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:08:09.590Z", - baseModelName: "gpt-4o-audio-preview", + "provider": "openai", + "description": "GPT-4o variant with native audio input and output capabilities via the Chat Completions API, supporting both text and audio modalities for conversational and voice-based applications.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "audio_input", + "audio_output", + "tool_use", + "streaming" + ], + "releaseDate": "2024-10-01", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": "2026-05-07", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:08:09.590Z", + "baseModelName": "gpt-4o-audio-preview" }, "gpt-4o-mini": { - provider: "openai", - description: - "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2024-07-18", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:09:50.130Z", - baseModelName: null, + "provider": "openai", + "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2024-07-18", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:09:50.130Z", + "baseModelName": null }, "gpt-4o-mini-2024-07-18": { - provider: "openai", - description: - "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "fine_tunable"], - releaseDate: "2024-07-18", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:09:50.130Z", - baseModelName: "gpt-4o-mini", + "provider": "openai", + "description": "Fast, affordable small model optimized for focused tasks. Positioned as OpenAI's cost-efficient option with strong performance on benchmarks relative to its size.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "fine_tunable" + ], + "releaseDate": "2024-07-18", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:09:50.130Z", + "baseModelName": "gpt-4o-mini" }, "gpt-4o-realtime-preview": { - provider: "openai", - description: - "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], - releaseDate: "2024-10-01", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: false, - deprecationDate: "2026-05-07", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:09:35.495Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "audio_input", + "audio_output", + "tool_use", + "streaming" + ], + "releaseDate": "2024-10-01", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": false, + "deprecationDate": "2026-05-07", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:09:35.495Z", + "baseModelName": null }, "gpt-4o-realtime-preview-2024-10-01": { - provider: "openai", - description: - "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", - contextWindow: 128000, - maxOutputTokens: 4096, - capabilities: ["audio_input", "audio_output", "tool_use", "streaming"], - releaseDate: "2024-10-01", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: false, - deprecationDate: "2026-05-07", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:09:35.495Z", - baseModelName: "gpt-4o-realtime-preview", + "provider": "openai", + "description": "OpenAI's real-time multimodal model capable of processing and generating both text and audio over WebRTC or WebSocket, enabling low-latency voice conversations and audio interactions.", + "contextWindow": 128000, + "maxOutputTokens": 4096, + "capabilities": [ + "audio_input", + "audio_output", + "tool_use", + "streaming" + ], + "releaseDate": "2024-10-01", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": false, + "deprecationDate": "2026-05-07", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:09:35.495Z", + "baseModelName": "gpt-4o-realtime-preview" }, "gpt-5": { - provider: "openai", - description: - "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "image_generation", - "code_execution", + "code_execution" ], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-09-30", - resolvedAt: "2026-03-24T11:09:28.216Z", - baseModelName: null, + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-09-30", + "resolvedAt": "2026-03-24T11:09:28.216Z", + "baseModelName": null }, "gpt-5-2025-08-07": { - provider: "openai", - description: - "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: [ + "provider": "openai", + "description": "OpenAI's flagship reasoning model released August 2025, featuring a 400K token context window with strong coding, reasoning, and agentic capabilities.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ "vision", "tool_use", "streaming", "json_mode", "image_generation", - "code_execution", + "code_execution" ], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-09-30", - resolvedAt: "2026-03-24T11:09:28.216Z", - baseModelName: "gpt-5", + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-09-30", + "resolvedAt": "2026-03-24T11:09:28.216Z", + "baseModelName": "gpt-5" }, "gpt-5-chat-latest": { - provider: "openai", - description: - "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", - contextWindow: 128000, - maxOutputTokens: 16384, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-09-30", - resolvedAt: "2026-03-24T11:09:24.834Z", - baseModelName: "gpt-5-chat", + "provider": "openai", + "description": "Non-reasoning GPT-5 model used in ChatGPT, optimized for conversational tasks. Supports text and image inputs with function calling and structured outputs.", + "contextWindow": 128000, + "maxOutputTokens": 16384, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-09-30", + "resolvedAt": "2026-03-24T11:09:24.834Z", + "baseModelName": "gpt-5-chat" }, "gpt-5-mini": { - provider: "openai", - description: - "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-05-31", - resolvedAt: "2026-03-24T11:09:42.822Z", - baseModelName: null, + "provider": "openai", + "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-05-31", + "resolvedAt": "2026-03-24T11:09:42.822Z", + "baseModelName": null }, "gpt-5-mini-2025-08-07": { - provider: "openai", - description: - "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-05-31", - resolvedAt: "2026-03-24T11:09:42.822Z", - baseModelName: "gpt-5-mini", + "provider": "openai", + "description": "A faster, more cost-efficient version of GPT-5 designed for well-defined tasks and precise prompts. Supports reasoning with configurable effort levels and offers reduced latency compared to the full GPT-5 model.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-05-31", + "resolvedAt": "2026-03-24T11:09:42.822Z", + "baseModelName": "gpt-5-mini" }, "gpt-5-nano": { - provider: "openai", - description: - "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-05-31", - resolvedAt: "2026-03-24T11:11:24.884Z", - baseModelName: null, + "provider": "openai", + "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-05-31", + "resolvedAt": "2026-03-24T11:11:24.884Z", + "baseModelName": null }, "gpt-5-nano-2025-08-07": { - provider: "openai", - description: - "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-08-07", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-05-31", - resolvedAt: "2026-03-24T11:11:24.884Z", - baseModelName: "gpt-5-nano", + "provider": "openai", + "description": "The smallest and fastest variant in the GPT-5 family, optimized for developer tools, rapid interactions, and ultra-low latency environments. Best suited for classification, data extraction, ranking, and sub-agent tasks.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-08-07", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-05-31", + "resolvedAt": "2026-03-24T11:11:24.884Z", + "baseModelName": "gpt-5-nano" }, "gpt-5-pro": { - provider: "openai", - description: - "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-10-06", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-10-01", - resolvedAt: "2026-03-24T11:11:37.048Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-10-06", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-10-01", + "resolvedAt": "2026-03-24T11:11:37.048Z", + "baseModelName": null }, "gpt-5-pro-2025-10-06": { - provider: "openai", - description: - "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-10-06", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-10-01", - resolvedAt: "2026-03-24T11:11:37.048Z", - baseModelName: "gpt-5-pro", + "provider": "openai", + "description": "OpenAI's enhanced GPT-5 variant optimized for complex tasks requiring step-by-step reasoning, with reduced hallucination and improved code quality compared to the base GPT-5.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-10-06", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-10-01", + "resolvedAt": "2026-03-24T11:11:37.048Z", + "baseModelName": "gpt-5-pro" }, "gpt-5.1": { - provider: "openai", - description: - "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-11-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-09-30", - resolvedAt: "2026-03-24T11:11:47.327Z", - baseModelName: null, + "provider": "openai", + "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-11-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-09-30", + "resolvedAt": "2026-03-24T11:11:47.327Z", + "baseModelName": null }, "gpt-5.1-2025-11-13": { - provider: "openai", - description: - "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2025-11-13", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-09-30", - resolvedAt: "2026-03-24T11:11:47.327Z", - baseModelName: "gpt-5.1", + "provider": "openai", + "description": "GPT-5.1 is OpenAI's frontier-grade model in the GPT-5 series, offering adaptive reasoning with configurable effort levels, improved coding and math performance, and a more natural conversational style compared to GPT-5.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2025-11-13", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-09-30", + "resolvedAt": "2026-03-24T11:11:47.327Z", + "baseModelName": "gpt-5.1" }, "gpt-5.2": { - provider: "openai", - description: - "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-12-11", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:11:13.129Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-12-11", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:11:13.129Z", + "baseModelName": null }, "gpt-5.2-2025-12-11": { - provider: "openai", - description: - "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-12-11", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:11:13.129Z", - baseModelName: "gpt-5.2", + "provider": "openai", + "description": "OpenAI's flagship multimodal model released December 2025, excelling at long-context reasoning, agentic tool use, software engineering, and professional knowledge work. Available in Instant, Thinking, and Pro variants.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-12-11", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:11:13.129Z", + "baseModelName": "gpt-5.2" }, "gpt-5.2-pro": { - provider: "openai", - description: - "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], - releaseDate: "2025-12-11", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:11:12.711Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "extended_thinking" + ], + "releaseDate": "2025-12-11", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:11:12.711Z", + "baseModelName": null }, "gpt-5.2-pro-2025-12-11": { - provider: "openai", - description: - "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], - releaseDate: "2025-12-11", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:11:12.711Z", - baseModelName: "gpt-5.2-pro", + "provider": "openai", + "description": "OpenAI's previous pro-tier reasoning model optimized for complex professional work requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Superseded by GPT-5.4 pro.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "extended_thinking" + ], + "releaseDate": "2025-12-11", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:11:12.711Z", + "baseModelName": "gpt-5.2-pro" }, "gpt-5.4": { - provider: "openai", - description: - "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", - contextWindow: 1050000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution"], - releaseDate: "2026-03-05", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:09.220Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", + "contextWindow": 1050000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "code_execution" + ], + "releaseDate": "2026-03-05", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:09.220Z", + "baseModelName": null }, "gpt-5.4-2026-03-05": { - provider: "openai", - description: - "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", - contextWindow: 1050000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "code_execution"], - releaseDate: "2026-03-05", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:09.220Z", - baseModelName: "gpt-5.4", + "provider": "openai", + "description": "OpenAI's most capable frontier model as of March 2026, featuring state-of-the-art coding, native computer-use capabilities, and a 1M-token context window for professional and agentic workflows.", + "contextWindow": 1050000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "code_execution" + ], + "releaseDate": "2026-03-05", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:09.220Z", + "baseModelName": "gpt-5.4" }, "gpt-5.4-mini": { - provider: "openai", - description: - "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2026-03-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:35.473Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2026-03-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:35.473Z", + "baseModelName": null }, "gpt-5.4-mini-2026-03-17": { - provider: "openai", - description: - "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2026-03-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:35.473Z", - baseModelName: "gpt-5.4-mini", + "provider": "openai", + "description": "OpenAI's fast and efficient small model from the GPT-5.4 family, designed for high-volume workloads. Approaches GPT-5.4 performance on coding and reasoning while running over 2x faster than GPT-5 mini.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2026-03-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:35.473Z", + "baseModelName": "gpt-5.4-mini" }, "gpt-5.4-nano": { - provider: "openai", - description: - "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2026-03-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:52.285Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2026-03-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:52.285Z", + "baseModelName": null }, "gpt-5.4-nano-2026-03-17": { - provider: "openai", - description: - "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", - contextWindow: 400000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2026-03-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:52.285Z", - baseModelName: "gpt-5.4-nano", + "provider": "openai", + "description": "OpenAI's cheapest GPT-5.4-class model optimized for simple high-volume tasks like classification, data extraction, ranking, and sub-agent delegation in agentic workflows.", + "contextWindow": 400000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2026-03-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:52.285Z", + "baseModelName": "gpt-5.4-nano" }, "gpt-5.4-pro": { - provider: "openai", - description: - "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", - contextWindow: 1050000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], - releaseDate: "2026-03-05", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:56.903Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", + "contextWindow": 1050000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "extended_thinking" + ], + "releaseDate": "2026-03-05", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:56.903Z", + "baseModelName": null }, "gpt-5.4-pro-2026-03-05": { - provider: "openai", - description: - "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", - contextWindow: 1050000, - maxOutputTokens: 128000, - capabilities: ["vision", "tool_use", "streaming", "extended_thinking"], - releaseDate: "2026-03-05", - isHidden: false, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2025-08-31", - resolvedAt: "2026-03-24T11:12:56.903Z", - baseModelName: "gpt-5.4-pro", - }, - o1: { - provider: "openai", - description: - "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-12-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:12:23.948Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's highest-capability GPT-5.4 variant, using additional compute for harder problems. Available via Responses API only, designed for complex reasoning, coding, and agentic workflows.", + "contextWindow": 1050000, + "maxOutputTokens": 128000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "extended_thinking" + ], + "releaseDate": "2026-03-05", + "isHidden": false, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2025-08-31", + "resolvedAt": "2026-03-24T11:12:56.903Z", + "baseModelName": "gpt-5.4-pro" + }, + "o1": { + "provider": "openai", + "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-12-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:12:23.948Z", + "baseModelName": null }, "o1-2024-12-17": { - provider: "openai", - description: - "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode"], - releaseDate: "2024-12-17", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:12:23.948Z", - baseModelName: "o1", + "provider": "openai", + "description": "OpenAI's reasoning model designed for complex tasks requiring multi-step logical thinking, excelling at math, science, and coding problems.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode" + ], + "releaseDate": "2024-12-17", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:12:23.948Z", + "baseModelName": "o1" }, "o1-mini": { - provider: "openai", - description: - "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", - contextWindow: 128000, - maxOutputTokens: 65536, - capabilities: ["streaming", "json_mode"], - releaseDate: "2024-09-12", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-06-30", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:12:37.030Z", - baseModelName: null, + "provider": "openai", + "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", + "contextWindow": 128000, + "maxOutputTokens": 65536, + "capabilities": [ + "streaming", + "json_mode" + ], + "releaseDate": "2024-09-12", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-06-30", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:12:37.030Z", + "baseModelName": null }, "o1-mini-2024-09-12": { - provider: "openai", - description: - "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", - contextWindow: 128000, - maxOutputTokens: 65536, - capabilities: ["streaming", "json_mode"], - releaseDate: "2024-09-12", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-06-30", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:12:37.030Z", - baseModelName: "o1-mini", + "provider": "openai", + "description": "A smaller, faster, and cheaper reasoning model in OpenAI's o1 series, optimized for coding, math, and science tasks requiring multi-step reasoning.", + "contextWindow": 128000, + "maxOutputTokens": 65536, + "capabilities": [ + "streaming", + "json_mode" + ], + "releaseDate": "2024-09-12", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-06-30", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:12:37.030Z", + "baseModelName": "o1-mini" }, "o1-preview": { - provider: "openai", - description: - "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", - contextWindow: 128000, - maxOutputTokens: 32768, - capabilities: ["streaming"], - releaseDate: "2024-09-12", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-10-31", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:12:59.198Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", + "contextWindow": 128000, + "maxOutputTokens": 32768, + "capabilities": [ + "streaming" + ], + "releaseDate": "2024-09-12", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-10-31", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:12:59.198Z", + "baseModelName": null }, "o1-preview-2024-09-12": { - provider: "openai", - description: - "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", - contextWindow: 128000, - maxOutputTokens: 32768, - capabilities: ["streaming"], - releaseDate: "2024-09-12", - isHidden: true, - supportsStructuredOutput: false, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: "2025-10-31", - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:12:59.198Z", - baseModelName: "o1-preview", + "provider": "openai", + "description": "OpenAI's first reasoning model using chain-of-thought to solve complex problems in science, coding, and math. Predecessor to o1 and o3 series.", + "contextWindow": 128000, + "maxOutputTokens": 32768, + "capabilities": [ + "streaming" + ], + "releaseDate": "2024-09-12", + "isHidden": true, + "supportsStructuredOutput": false, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": "2025-10-31", + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:12:59.198Z", + "baseModelName": "o1-preview" }, "o1-pro": { - provider: "openai", - description: - "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "json_mode", "extended_thinking"], - releaseDate: "2025-03-19", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:13:57.532Z", - baseModelName: null, + "provider": "openai", + "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-03-19", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:13:57.532Z", + "baseModelName": null }, "o1-pro-2025-03-19": { - provider: "openai", - description: - "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "json_mode", "extended_thinking"], - releaseDate: "2025-03-19", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2023-10-01", - resolvedAt: "2026-03-24T11:13:57.532Z", - baseModelName: "o1-pro", - }, - o3: { - provider: "openai", - description: - "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-04-16", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:14:04.906Z", - baseModelName: null, + "provider": "openai", + "description": "A version of OpenAI's o1 reasoning model that uses significantly more compute to deliver better, more consistent answers on complex reasoning tasks in science, coding, and math.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-03-19", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2023-10-01", + "resolvedAt": "2026-03-24T11:13:57.532Z", + "baseModelName": "o1-pro" + }, + "o3": { + "provider": "openai", + "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-04-16", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:14:04.906Z", + "baseModelName": null }, "o3-2025-04-16": { - provider: "openai", - description: - "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-04-16", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:14:04.906Z", - baseModelName: "o3", + "provider": "openai", + "description": "OpenAI's advanced reasoning model designed for complex tasks requiring deep reasoning, excelling at software engineering, mathematics, scientific reasoning, and visual reasoning tasks.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-04-16", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:14:04.906Z", + "baseModelName": "o3" }, "o3-mini": { - provider: "openai", - description: - "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-01-31", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:13:33.788Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-01-31", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:13:33.788Z", + "baseModelName": null }, "o3-mini-2025-01-31": { - provider: "openai", - description: - "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-01-31", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2025-01-01", - resolvedAt: "2026-03-24T11:13:33.788Z", - baseModelName: "o3-mini", + "provider": "openai", + "description": "OpenAI's compact reasoning model optimized for STEM tasks, offering strong performance in math, science, and coding at lower cost than o3.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-01-31", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2025-01-01", + "resolvedAt": "2026-03-24T11:13:33.788Z", + "baseModelName": "o3-mini" }, "o3-pro": { - provider: "openai", - description: - "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-06-10", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:14:10.900Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-06-10", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:14:10.900Z", + "baseModelName": null }, "o3-pro-2025-06-10": { - provider: "openai", - description: - "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-06-10", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: false, - supportsStreamingToolCalls: false, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:14:10.900Z", - baseModelName: "o3-pro", + "provider": "openai", + "description": "OpenAI's most reliable reasoning model, a version of o3 designed to think longer and provide more consistently accurate answers for challenging math, science, and coding problems.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-06-10", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": false, + "supportsStreamingToolCalls": false, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:14:10.900Z", + "baseModelName": "o3-pro" }, "o4-mini": { - provider: "openai", - description: - "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-04-16", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:14:16.050Z", - baseModelName: null, + "provider": "openai", + "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-04-16", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:14:16.050Z", + "baseModelName": null }, "o4-mini-2025-04-16": { - provider: "openai", - description: - "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", - contextWindow: 200000, - maxOutputTokens: 100000, - capabilities: ["vision", "tool_use", "streaming", "json_mode", "extended_thinking"], - releaseDate: "2025-04-16", - isHidden: false, - supportsStructuredOutput: true, - supportsParallelToolCalls: true, - supportsStreamingToolCalls: true, - deprecationDate: null, - knowledgeCutoff: "2024-06-01", - resolvedAt: "2026-03-24T11:14:16.050Z", - baseModelName: "o4-mini", - }, + "provider": "openai", + "description": "OpenAI's small reasoning model optimized for fast, cost-efficient reasoning with strong performance in math, coding, and visual tasks.", + "contextWindow": 200000, + "maxOutputTokens": 100000, + "capabilities": [ + "vision", + "tool_use", + "streaming", + "json_mode", + "extended_thinking" + ], + "releaseDate": "2025-04-16", + "isHidden": false, + "supportsStructuredOutput": true, + "supportsParallelToolCalls": true, + "supportsStreamingToolCalls": true, + "deprecationDate": null, + "knowledgeCutoff": "2024-06-01", + "resolvedAt": "2026-03-24T11:14:16.050Z", + "baseModelName": "o4-mini" + } }; diff --git a/internal-packages/replication/src/client.ts b/internal-packages/replication/src/client.ts index a45831dc57e..1a128a41e80 100644 --- a/internal-packages/replication/src/client.ts +++ b/internal-packages/replication/src/client.ts @@ -35,6 +35,12 @@ export interface LogicalReplicationClientOptions { * The name of the publication to use. */ publicationName: string; + /** + * Whether to create the publication with `publish_via_partition_root = true`. + * Required when the replicated table is a partitioned parent: without it, child + * partitions publish nothing and the slot stays silent (default: false). + */ + publishViaPartitionRoot?: boolean; /** * A connected Redis client instance for Redlock. */ @@ -616,12 +622,18 @@ export class LogicalReplicationClient { return true; } + const publicationWithOptions: string[] = []; + if (this.options.publicationActions) { + publicationWithOptions.push(`publish = '${this.options.publicationActions.join(", ")}'`); + } + if (this.options.publishViaPartitionRoot) { + publicationWithOptions.push("publish_via_partition_root = true"); + } + const [createError] = await tryCatch( this.client.query( `CREATE PUBLICATION "${this.options.publicationName}" FOR TABLE "${this.options.table}" ${ - this.options.publicationActions - ? `WITH (publish = '${this.options.publicationActions.join(", ")}')` - : "" + publicationWithOptions.length > 0 ? `WITH (${publicationWithOptions.join(", ")})` : "" };` ) ); diff --git a/internal-packages/testcontainers/src/index.ts b/internal-packages/testcontainers/src/index.ts index e1cd3d25aea..9f624c688ae 100644 --- a/internal-packages/testcontainers/src/index.ts +++ b/internal-packages/testcontainers/src/index.ts @@ -28,7 +28,11 @@ import { withContainerSetup, } from "./utils"; -export { assertNonNullable, createPostgresContainer } from "./utils"; +export { + assertNonNullable, + createPostgresContainer, + createStandalonePostgresContainer, +} from "./utils"; export { laggingReplica, type LaggingModel } from "./laggingReplica"; export { logCleanup }; export type { MinIOConnectionConfig }; diff --git a/internal-packages/testcontainers/src/utils.ts b/internal-packages/testcontainers/src/utils.ts index 385b4113f59..ae5897b69a8 100644 --- a/internal-packages/testcontainers/src/utils.ts +++ b/internal-packages/testcontainers/src/utils.ts @@ -161,6 +161,20 @@ export async function createPostgresContainer( return { url: container.getConnectionUri(), container, network }; } +/** + * A second schema-loaded Postgres on its own container (no shared network alias), for tests that + * exercise a split across two databases. The caller owns stopping the returned container. + */ +export async function createStandalonePostgresContainer() { + const container = await withCiResourceLimits(new PostgreSqlContainer("docker.io/postgres:14")) + .withCommand(["-c", "listen_addresses=*", "-c", "wal_level=logical"]) + .start(); + + await pushDatabaseSchema(container.getConnectionUri()); + + return { url: container.getConnectionUri(), container }; +} + export async function createClickHouseContainer(network: StartedNetwork) { const container = await withCiResourceLimits(new ClickHouseContainer()) .withNetwork(network) diff --git a/internal-packages/webhook-engine/package.json b/internal-packages/webhook-engine/package.json new file mode 100644 index 00000000000..0490d56e946 --- /dev/null +++ b/internal-packages/webhook-engine/package.json @@ -0,0 +1,37 @@ +{ + "name": "@internal/webhook-engine", + "private": true, + "version": "0.0.1", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "type": "module", + "exports": { + ".": { + "@triggerdotdev/source": "./src/index.ts", + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + } + }, + "dependencies": { + "@internal/redis": "workspace:*", + "@trigger.dev/redis-worker": "workspace:*", + "@internal/tracing": "workspace:*", + "@trigger.dev/core": "workspace:*", + "@trigger.dev/database": "workspace:*", + "nanoid": "3.3.8", + "zod": "3.25.76" + }, + "devDependencies": { + "@internal/testcontainers": "workspace:*", + "rimraf": "6.0.1" + }, + "scripts": { + "clean": "rimraf dist", + "typecheck": "tsc --noEmit -p tsconfig.build.json", + "test": "vitest --sequence.concurrent=false --no-file-parallelism", + "test:coverage": "vitest --sequence.concurrent=false --no-file-parallelism --coverage.enabled", + "build": "pnpm run clean && tsc -p tsconfig.build.json", + "dev": "tsc --watch -p tsconfig.build.json" + } +} diff --git a/internal-packages/webhook-engine/src/engine/cache.ts b/internal-packages/webhook-engine/src/engine/cache.ts new file mode 100644 index 00000000000..e0f7c241160 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/cache.ts @@ -0,0 +1,49 @@ +import type { FilterAst } from "@trigger.dev/core/v3"; +import type { WebhookEndpoint } from "@trigger.dev/database"; + +// What the ingest hot path needs to verify + persist a delivery, resolved once and reused: the +// endpoint row, its plaintext signing secret, and its compiled filter AST (parsed once here, null = +// route all). All immutable per endpoint within the cache TTL. +export type CachedEndpoint = { + endpoint: WebhookEndpoint; + secret: string; + filterAst: FilterAst | null; +}; + +// Minimal in-memory TTL cache with FIFO eviction at maxSize. ttlMs <= 0 disables it (every get misses). +export class TtlCache { + private readonly map = new Map(); + + constructor( + private readonly ttlMs: number, + private readonly maxSize: number + ) {} + + get enabled(): boolean { + return this.ttlMs > 0; + } + + get(key: string): V | undefined { + if (!this.enabled) return undefined; + const entry = this.map.get(key); + if (!entry) return undefined; + if (entry.expiresAt <= Date.now()) { + this.map.delete(key); + return undefined; + } + return entry.value; + } + + set(key: string, value: V): void { + if (!this.enabled) return; + if (this.map.size >= this.maxSize && !this.map.has(key)) { + const oldest = this.map.keys().next().value; + if (oldest !== undefined) this.map.delete(oldest); + } + this.map.set(key, { value, expiresAt: Date.now() + this.ttlMs }); + } + + delete(key: string): void { + this.map.delete(key); + } +} diff --git a/internal-packages/webhook-engine/src/engine/filter/evaluate.ts b/internal-packages/webhook-engine/src/engine/filter/evaluate.ts new file mode 100644 index 00000000000..db3106f9924 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/filter/evaluate.ts @@ -0,0 +1,185 @@ +import type { FilterAst, FilterOp, FilterScalar, FilterValue } from "@trigger.dev/core/v3"; +import type { FilterContext, FilterMatch } from "./types.js"; + +// Evaluate a compiled filter against a delivery. Pure + deterministic (retry-safe). On no-match, +// `reason` names the representative failing clause + the actual value, for WebhookDelivery.filterReason. + +export function evaluateFilter(ast: FilterAst, ctx: FilterContext): FilterMatch { + return evalNode(ast, ctx); +} + +function evalNode(node: FilterAst, ctx: FilterContext): FilterMatch { + if (node.kind === "and") { + for (const clause of node.clauses) { + const r = evalNode(clause, ctx); + if (!r.match) return r; + } + return { match: true }; + } + if (node.kind === "or") { + let firstReason: string | undefined; + for (const clause of node.clauses) { + const r = evalNode(clause, ctx); + if (r.match) return { match: true }; + firstReason ??= r.reason; + } + return { match: false, reason: firstReason ?? "no condition matched" }; + } + if (node.kind === "quantifier") { + const arr = resolvePath(node.path, ctx); + const sub = node.clause; + const label = `${sub.path} ${OP_LABEL[sub.op]} ${JSON.stringify(sub.value)}`; + if (!Array.isArray(arr)) { + return { match: false, reason: `${node.path} is not an array` }; + } + const test = (el: unknown) => applyOp(sub.op, resolveRelative(el, sub.path), sub.value); + const ok = node.mode === "any" ? arr.some(test) : arr.every(test); + return ok + ? { match: true } + : { + match: false, + reason: + node.mode === "any" + ? `no element of ${node.path} satisfies ${label}` + : `not every element of ${node.path} satisfies ${label}`, + }; + } + const actual = resolvePath(node.path, ctx); + if (node.valueRef !== undefined) { + const rhs = resolvePath(node.valueRef, ctx); + return applyOpRef(node.op, actual, rhs) + ? { match: true } + : { + match: false, + reason: `${node.path} is ${JSON.stringify(actual)}, expected ${OP_LABEL[node.op]} ${ + node.valueRef + } (${JSON.stringify(rhs)})`, + }; + } + return applyOp(node.op, actual, node.value as FilterValue) + ? { match: true } + : { match: false, reason: describe(node.path, node.op, actual, node.value as FilterValue) }; +} + +// Field-to-field comparison (both operands resolved from the event). Equality is strict on the +// resolved values; ordering coerces to number. Non-comparison ops are rejected by the parser/types. +function applyOpRef(op: FilterOp, a: unknown, b: unknown): boolean { + switch (op) { + case "eq": + return a === b; + case "neq": + return a !== b; + case "gt": + return numCompare(a, b, (x, y) => x > y); + case "lt": + return numCompare(a, b, (x, y) => x < y); + case "gte": + return numCompare(a, b, (x, y) => x >= y); + case "lte": + return numCompare(a, b, (x, y) => x <= y); + default: + return false; + } +} + +function resolvePath(path: string, ctx: FilterContext): unknown { + const segs = path.split("."); + const [ns, ...rest] = segs; + if (ns === "header") return lookupHeaderCI(ctx.headers, rest.join(".")); + let cur: unknown = ns === "event" ? ctx.event : ns === "webhook" ? ctx.webhook : undefined; + for (const seg of rest) { + if (cur == null || typeof cur !== "object") return undefined; + cur = (cur as Record)[seg]; + } + return cur; +} + +// Resolve a bare (element-relative) dotted path against an object, used for quantifier sub-clauses. +function resolveRelative(obj: unknown, path: string): unknown { + let cur: unknown = obj; + for (const seg of path.split(".")) { + if (cur == null || typeof cur !== "object") return undefined; + cur = (cur as Record)[seg]; + } + return cur; +} + +function lookupHeaderCI(headers: Record, name: string): string | undefined { + const lower = name.toLowerCase(); + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === lower) return headers[key]; + } + return undefined; +} + +function applyOp(op: FilterOp, actual: unknown, value: FilterValue): boolean { + const missing = actual === undefined; + switch (op) { + case "eq": + return looseEq(actual, value as FilterScalar); + case "neq": + return !looseEq(actual, value as FilterScalar); + case "gt": + return numCompare(actual, value, (a, b) => a > b); + case "lt": + return numCompare(actual, value, (a, b) => a < b); + case "gte": + return numCompare(actual, value, (a, b) => a >= b); + case "lte": + return numCompare(actual, value, (a, b) => a <= b); + case "in": + return Array.isArray(value) && value.some((v) => looseEq(actual, v)); + case "nin": + return !(Array.isArray(value) && value.some((v) => looseEq(actual, v))); + case "startsWith": + return !missing && typeof value === "string" && String(actual).startsWith(value); + case "endsWith": + return !missing && typeof value === "string" && String(actual).endsWith(value); + case "contains": + return !missing && typeof value === "string" && String(actual).includes(value); + } +} + +// Literal-type-driven comparison, to tame JSON's loose typing. A missing field equals only `null`. +function looseEq(actual: unknown, lit: FilterScalar): boolean { + if (lit === null) return actual === null || actual === undefined; + if (actual === undefined) return false; + if (typeof lit === "string") return String(actual) === lit; + if (typeof lit === "number") { + const n = typeof actual === "number" ? actual : Number(actual); + return !Number.isNaN(n) && n === lit; + } + return actual === lit; // boolean +} + +function coerceNum(x: unknown): number { + return typeof x === "number" ? x : typeof x === "string" ? Number(x) : NaN; +} + +function numCompare( + actual: unknown, + value: unknown, + cmp: (a: number, b: number) => boolean +): boolean { + const a = coerceNum(actual); + const b = coerceNum(value); + return !Number.isNaN(a) && !Number.isNaN(b) && cmp(a, b); +} + +const OP_LABEL: Record = { + eq: "==", + neq: "!=", + gt: ">", + lt: "<", + gte: ">=", + lte: "<=", + in: "in", + nin: "not in", + startsWith: "startsWith", + endsWith: "endsWith", + contains: "contains", +}; + +function describe(path: string, op: FilterOp, actual: unknown, value: FilterValue): string { + return `${path} is ${JSON.stringify(actual)}, expected ${OP_LABEL[op]} ${JSON.stringify(value)}`; +} diff --git a/internal-packages/webhook-engine/src/engine/filter/filter.test.ts b/internal-packages/webhook-engine/src/engine/filter/filter.test.ts new file mode 100644 index 00000000000..4a86e15193c --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/filter/filter.test.ts @@ -0,0 +1,218 @@ +import { describe, expect, it } from "vitest"; +import { evaluateFilter, FilterParseError, parseFilter } from "./index.js"; +import type { FilterContext } from "./index.js"; + +const ctx = (over: Partial = {}): FilterContext => ({ + event: { + action: "created", + issue: { number: 200, author: "alice" }, + repository: { full_name: "myorg/repo", private: false }, + }, + headers: { "X-GitHub-Event": "issue_comment", "Content-Type": "application/json" }, + webhook: { externalRef: "install_42", tenantId: "t1", source: "github" }, + ...over, +}); + +const matches = (filter: string, over?: Partial) => + evaluateFilter(parseFilter(filter), ctx(over)).match; + +describe("parseFilter", () => { + it("parses a single clause", () => { + expect(parseFilter("event.action == 'created'")).toEqual({ + kind: "clause", + path: "event.action", + op: "eq", + value: "created", + }); + }); + + it("respects && binding tighter than || (DNF)", () => { + expect( + parseFilter( + "event.action == 'created' || event.action == 'edited' && event.repository.private == false" + ) + ).toEqual({ + kind: "or", + clauses: [ + { kind: "clause", path: "event.action", op: "eq", value: "created" }, + { + kind: "and", + clauses: [ + { kind: "clause", path: "event.action", op: "eq", value: "edited" }, + { kind: "clause", path: "event.repository.private", op: "eq", value: false }, + ], + }, + ], + }); + }); + + it("parses parens, lists, numbers, not in", () => { + expect(parseFilter("event.action in ['created', 'edited']").value).toEqual([ + "created", + "edited", + ]); + expect(parseFilter("event.issue.number > 100")).toMatchObject({ op: "gt", value: 100 }); + expect(parseFilter("event.action not in ['deleted']")).toMatchObject({ op: "nin" }); + expect(parseFilter("( event.action == 'created' )")).toMatchObject({ kind: "clause" }); + }); + + it("rejects malformed filters", () => { + for (const bad of [ + "event.action", + "event.action = 'x'", + "foo.bar == 'x'", + "event.action == 'created' && ( event.action == 'edited'", + "", + ]) { + expect(() => parseFilter(bad), bad).toThrow(FilterParseError); + } + }); + + it("enforces the soft clause cap", () => { + const big = Array.from({ length: 60 }, () => "event.action == 'created'").join(" && "); + expect(() => parseFilter(big)).toThrow(/too complex/); + }); +}); + +describe("evaluateFilter", () => { + it("matches equality, and, or, in, numeric, parens", () => { + expect(matches("event.action == 'created'")).toBe(true); + expect(matches("event.action == 'created' && event.repository.private == false")).toBe(true); + expect(matches("event.action == 'edited' || event.action == 'created'")).toBe(true); + expect(matches("event.action in ['edited', 'created']")).toBe(true); + expect(matches("event.issue.number > 100")).toBe(true); + expect(matches("event.issue.number > 500")).toBe(false); + expect( + matches( + "( event.action == 'edited' || event.action == 'created' ) && event.repository.private == false" + ) + ).toBe(true); + }); + + it("looks up headers case-insensitively", () => { + expect(matches("header.x-github-event == 'issue_comment'")).toBe(true); + }); + + it("resolves the webhook namespace", () => { + expect(matches("webhook.externalRef == 'install_42'")).toBe(true); + expect(matches("webhook.externalRef == 'other'")).toBe(false); + }); + + it("treats a missing field correctly", () => { + expect(matches("event.assignee == 'bob'")).toBe(false); + expect(matches("event.assignee != 'bob'")).toBe(true); + expect(matches("event.assignee == null")).toBe(true); + }); + + it("string operators", () => { + expect(matches("event.repository.full_name startsWith 'myorg/'")).toBe(true); + expect(matches("event.repository.full_name contains 'repo'")).toBe(true); + expect(matches("event.repository.full_name endsWith '/other'")).toBe(false); + }); + + it("field-to-field: parses a path RHS as a valueRef and compares two event fields", () => { + expect(parseFilter("event.issue.author == event.repository.full_name")).toEqual({ + kind: "clause", + path: "event.issue.author", + op: "eq", + valueRef: "event.repository.full_name", + }); + expect(matches("event.issue.author == event.issue.author")).toBe(true); + expect(matches("event.issue.author == event.repository.full_name")).toBe(false); + expect(matches("event.issue.number >= event.issue.number")).toBe(true); + }); + + it("returns a reason naming the failing clause", () => { + const r = evaluateFilter( + parseFilter("event.action == 'created' && event.issue.number > 500"), + ctx() + ); + expect(r.match).toBe(false); + expect(r.reason).toContain("event.issue.number"); + expect(r.reason).toContain("> 500"); + }); +}); + +describe("quantifiers", () => { + const arrCtx = (): FilterContext => ({ + event: { + action: "push", + commits: [{ message: "fix bug" }, { message: "deploy to prod" }], + labels: [{ name: "bug" }, { name: "urgent" }], + }, + headers: {}, + webhook: {}, + }); + const m = (filter: string) => evaluateFilter(parseFilter(filter), arrCtx()).match; + + it("parses a quantifier sub-clause", () => { + expect(parseFilter("event.commits any ( message startsWith 'deploy' )")).toEqual({ + kind: "quantifier", + mode: "any", + path: "event.commits", + clause: { path: "message", op: "startsWith", value: "deploy" }, + }); + }); + + it("evaluates any / all over array elements", () => { + expect(m("event.commits any ( message startsWith 'deploy' )")).toBe(true); + expect(m("event.commits any ( message startsWith 'nope' )")).toBe(false); + expect(m("event.labels all ( name != 'wontfix' )")).toBe(true); + expect(m("event.labels all ( name == 'bug' )")).toBe(false); + }); + + it("composes with top-level boolean operators", () => { + expect(m("event.action == 'push' && event.labels any ( name == 'urgent' )")).toBe(true); + }); + + it("a non-array path does not match", () => { + expect(evaluateFilter(parseFilter("event.action any ( x == 'y' )"), arrCtx()).match).toBe( + false + ); + }); +}); + +// The @trigger.dev/slack self-message loop guard: route only genuine human messages, drop the bot's +// own posts (bot_id present), edits/deletes, and Slack system messages. Proves the loop is closed. +describe("slack self-message guard", () => { + const GUARD = + "event.event.type == 'message' && event.event.bot_id == null && event.event.subtype in [null, 'file_share', 'thread_broadcast']"; + const ev = (event: Record): Partial => ({ event: { event } }); + + it("routes a human message in a thread", () => { + expect( + evaluateFilter(parseFilter(GUARD), ctx(ev({ type: "message", channel: "C1", ts: "1" }))).match + ).toBe(true); + }); + + it("drops a channel_join system message (bot invited to the channel)", () => { + expect( + evaluateFilter( + parseFilter(GUARD), + ctx(ev({ type: "message", subtype: "channel_join", text: "has joined" })) + ).match + ).toBe(false); + }); + + it("drops the bot's own post (bot_id present)", () => { + expect( + evaluateFilter( + parseFilter(GUARD), + ctx(ev({ type: "message", bot_id: "B123", text: "reply" })) + ).match + ).toBe(false); + }); + + it("drops an edit re-delivery (message_changed subtype)", () => { + expect( + evaluateFilter(parseFilter(GUARD), ctx(ev({ type: "message", subtype: "message_changed" }))) + .match + ).toBe(false); + }); + + it("drops an app_mention duplicate (dedupes to the message event)", () => { + expect( + evaluateFilter(parseFilter(GUARD), ctx(ev({ type: "app_mention", text: "hey" }))).match + ).toBe(false); + }); +}); diff --git a/internal-packages/webhook-engine/src/engine/filter/index.ts b/internal-packages/webhook-engine/src/engine/filter/index.ts new file mode 100644 index 00000000000..b2c741547ef --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/filter/index.ts @@ -0,0 +1,8 @@ +export { parseFilter } from "./parse.js"; +export { evaluateFilter } from "./evaluate.js"; +export { + FilterParseError, + MAX_FILTER_CLAUSES, + type FilterContext, + type FilterMatch, +} from "./types.js"; diff --git a/internal-packages/webhook-engine/src/engine/filter/parse.ts b/internal-packages/webhook-engine/src/engine/filter/parse.ts new file mode 100644 index 00000000000..ad586a08154 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/filter/parse.ts @@ -0,0 +1,222 @@ +import type { FilterAst, FilterOp, FilterScalar, FilterValue } from "@trigger.dev/core/v3"; +import { FilterParseError, MAX_FILTER_CLAUSES } from "./types.js"; + +// Runtime recursive-descent parser: filter string -> FilterAst. Mirrors the type-level grammar in +// core/schemas/webhookFilter.ts (kept in sync via the golden corpus in filter.test.ts). Runs at +// index/deploy time, never at ingest. Throws FilterParseError on any structural problem. + +const CMP_TO_OP: Record = { + "==": "eq", + "!=": "neq", + ">": "gt", + "<": "lt", + ">=": "gte", + "<=": "lte", +}; +const WORD_OPS: Record = { + in: "in", + startsWith: "startsWith", + endsWith: "endsWith", + contains: "contains", +}; +const NAMESPACES = new Set(["event", "header", "webhook"]); + +type Tok = { + t: + | "lparen" + | "rparen" + | "lbracket" + | "rbracket" + | "comma" + | "and" + | "or" + | "cmp" + | "word" + | "string" + | "number"; + v: string; +}; + +function tokenize(s: string): Tok[] { + const toks: Tok[] = []; + let i = 0; + while (i < s.length) { + const c = s[i]; + if (c === " " || c === "\t" || c === "\n") { + i++; + continue; + } + const simple = { + "(": "lparen", + ")": "rparen", + "[": "lbracket", + "]": "rbracket", + ",": "comma", + } as const; + if (c in simple) { + toks.push({ t: simple[c as keyof typeof simple], v: c }); + i++; + continue; + } + if (c === "&" && s[i + 1] === "&") { + toks.push({ t: "and", v: "&&" }); + i += 2; + continue; + } + if (c === "|" && s[i + 1] === "|") { + toks.push({ t: "or", v: "||" }); + i += 2; + continue; + } + if (c === "'") { + let j = i + 1; + while (j < s.length && s[j] !== "'") j++; + if (j >= s.length) throw new FilterParseError("unterminated string literal"); + toks.push({ t: "string", v: s.slice(i + 1, j) }); + i = j + 1; + continue; + } + const two = s.slice(i, i + 2); + if (two === "==" || two === "!=" || two === ">=" || two === "<=") { + toks.push({ t: "cmp", v: two }); + i += 2; + continue; + } + if (c === ">" || c === "<") { + toks.push({ t: "cmp", v: c }); + i++; + continue; + } + if (c === "-" || (c >= "0" && c <= "9")) { + const m = /^-?\d+(\.\d+)?/.exec(s.slice(i)); + if (m) { + toks.push({ t: "number", v: m[0] }); + i += m[0].length; + continue; + } + } + const w = /^[A-Za-z_][A-Za-z0-9_.-]*/.exec(s.slice(i)); + if (w) { + toks.push({ t: "word", v: w[0] }); + i += w[0].length; + continue; + } + throw new FilterParseError(`unexpected character "${c}"`); + } + return toks; +} + +export function parseFilter(input: string): FilterAst { + const toks = tokenize(input); + if (toks.length === 0) throw new FilterParseError("empty filter"); + let pos = 0; + let clauseCount = 0; + const peek = (): Tok | undefined => toks[pos]; + const next = (): Tok | undefined => toks[pos++]; + + function parseOr(): FilterAst { + const branches = [parseAnd()]; + while (peek()?.t === "or") { + next(); + branches.push(parseAnd()); + } + return branches.length === 1 ? branches[0] : { kind: "or", clauses: branches }; + } + function parseAnd(): FilterAst { + const branches = [parseUnary()]; + while (peek()?.t === "and") { + next(); + branches.push(parseUnary()); + } + return branches.length === 1 ? branches[0] : { kind: "and", clauses: branches }; + } + function parseUnary(): FilterAst { + if (peek()?.t === "lparen") { + next(); + const inner = parseOr(); + if (next()?.t !== "rparen") throw new FilterParseError('expected ")"'); + return inner; + } + return parseClause(); + } + function parseClause(): FilterAst { + if (++clauseCount > MAX_FILTER_CLAUSES) { + throw new FilterParseError( + `filter too complex (> ${MAX_FILTER_CLAUSES} clauses); split it or move logic to the handler` + ); + } + const pathTok = next(); + if (!pathTok || pathTok.t !== "word") throw new FilterParseError("expected a field path"); + const path = pathTok.v; + if (!NAMESPACES.has(path.split(".")[0])) { + throw new FilterParseError(`field must start with event./header./webhook., got "${path}"`); + } + // Quantifier: `arrPath any|all ( subPath op value )` — sub-clause resolves against each element. + const q = peek(); + if (q?.t === "word" && (q.v === "any" || q.v === "all")) { + next(); + if (next()?.t !== "lparen") throw new FilterParseError(`expected "(" after "${q.v}"`); + const subPathTok = next(); + if (!subPathTok || subPathTok.t !== "word") { + throw new FilterParseError("expected a field path inside the quantifier"); + } + const subOp = parseOp(subPathTok.v); + const subValue = parseScalar(); + if (next()?.t !== "rparen") throw new FilterParseError('expected ")"'); + return { + kind: "quantifier", + mode: q.v, + path, + clause: { path: subPathTok.v, op: subOp, value: subValue }, + }; + } + const op = parseOp(path); + // An unquoted, namespace-prefixed operand is a field reference (field-to-field), not a literal. + const operand = peek(); + if (operand?.t === "word" && NAMESPACES.has(operand.v.split(".")[0])) { + next(); + return { kind: "clause", path, op, valueRef: operand.v }; + } + return { kind: "clause", path, op, value: parseValue() }; + } + function parseOp(path: string): FilterOp { + const t = next(); + if (t?.t === "cmp") return CMP_TO_OP[t.v]; + if (t?.t === "word" && t.v === "not") { + if (next()?.v !== "in") throw new FilterParseError('expected "in" after "not"'); + return "nin"; + } + if (t?.t === "word" && t.v in WORD_OPS) return WORD_OPS[t.v]; + throw new FilterParseError(`expected an operator after "${path}"`); + } + function parseValue(): FilterValue { + if (peek()?.t === "lbracket") { + next(); + const items: FilterScalar[] = []; + while (peek() && peek()?.t !== "rbracket") { + items.push(parseScalar()); + if (peek()?.t === "comma") next(); + else break; + } + if (next()?.t !== "rbracket") throw new FilterParseError('expected "]"'); + return items; + } + return parseScalar(); + } + function parseScalar(): FilterScalar { + const t = next(); + if (!t) throw new FilterParseError("expected a value"); + if (t.t === "string") return t.v; + if (t.t === "number") return Number(t.v); + if (t.t === "word") { + if (t.v === "true") return true; + if (t.v === "false") return false; + if (t.v === "null") return null; + } + throw new FilterParseError(`unexpected value "${t.v}"`); + } + + const ast = parseOr(); + if (pos < toks.length) throw new FilterParseError(`unexpected trailing token "${peek()?.v}"`); + return ast; +} diff --git a/internal-packages/webhook-engine/src/engine/filter/types.ts b/internal-packages/webhook-engine/src/engine/filter/types.ts new file mode 100644 index 00000000000..5eda5df1e3a --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/filter/types.ts @@ -0,0 +1,19 @@ +// Evaluation context for a compiled webhook filter. `event` is the parsed body, `headers` are the +// inbound request headers (looked up case-insensitively), `webhook` is the endpoint metadata. +export type FilterContext = { + event: unknown; + headers: Record; + webhook: Record; +}; + +export type FilterMatch = { match: boolean; reason?: string }; + +export class FilterParseError extends Error { + constructor(message: string) { + super(message); + this.name = "FilterParseError"; + } +} + +// Soft cap, well below the type-level ~200-clause TS2589 wall, so authors get a clean message. +export const MAX_FILTER_CLAUSES = 50; diff --git a/internal-packages/webhook-engine/src/engine/index.ts b/internal-packages/webhook-engine/src/engine/index.ts new file mode 100644 index 00000000000..c05820a0ff5 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/index.ts @@ -0,0 +1,830 @@ +import { + Counter, + getMeter, + getTracer, + Histogram, + Meter, + startSpan, + Tracer, +} from "@internal/tracing"; +import { Logger } from "@trigger.dev/core/logger"; +import { Prisma, WebhookDatabase, WebhookDelivery, WebhookEndpoint } from "@trigger.dev/database"; +import { Worker, type JobHandlerParams } from "@trigger.dev/redis-worker"; +import { createRedisClient, Redis } from "@internal/redis"; +import { FilterAst, WebhookVerifierArtifact } from "@trigger.dev/core/v3"; +import type { WebhookRoutingTarget } from "@trigger.dev/core/v3"; +import { WebhookDeliveryId } from "@trigger.dev/core/v3/isomorphic"; +import { webhookWorkerCatalog } from "./workerCatalog.js"; +import { ensurePartitions } from "./partitions.js"; +import { type CachedEndpoint, TtlCache } from "./cache.js"; +import { evaluateFilter, parseFilter } from "./filter/index.js"; +import { verify } from "./verification/index.js"; +import { sha256Hex } from "./verification/util.js"; +import { deriveIdempotencyKey, tryParseJson } from "./verification/derive.js"; +import { + IngestInput, + IngestResult, + ReplayResult, + TriggerWebhookTaskCallback, + WebhookDeliverTaskErrorType, + WebhookEngineOptions, +} from "./types.js"; +import { evaluateSessionKeyTemplate, walkPath as resolveBodyPath } from "./sessionKey.js"; + +// The deliver job's retry budget (redis-worker DLQs after this many attempts). +const WEBHOOK_DELIVER_MAX_ATTEMPTS = webhookWorkerCatalog["webhook.deliver"].retry.maxAttempts; + +export class WebhookEngine { + private worker: Worker; + private logger: Logger; + private tracer: Tracer; + private meter: Meter; + + private deliveryEnqueueCounter: Counter; + private deliveryFilteredCounter: Counter; + private deliveryExecutionCounter: Counter; + private deliveryExecutionDuration: Histogram; + private deliveryExecutionFailureCounter: Counter; + private ensurePartitionsCounter: Counter; + private endpointCacheCounter: Counter; + + prisma: WebhookDatabase; + + private triggerTask: TriggerWebhookTaskCallback; + private frontGate: Redis; + // Caches the endpoint + resolved signing secret per opaqueId so the ingest hot path skips two + // Postgres reads (endpoint lookup + secret decrypt). Both are immutable per endpoint within the + // TTL; a status change or secret rotation takes effect after at most the TTL. + private endpointCache: TtlCache; + + constructor(private readonly options: WebhookEngineOptions) { + this.logger = + options.logger ?? new Logger("WebhookEngine", (this.options.logLevel ?? "info") as any); + this.prisma = options.prisma; + this.triggerTask = options.triggerTask; + this.frontGate = createRedisClient(options.redis); + + this.tracer = options.tracer ?? getTracer("webhook-engine"); + this.meter = options.meter ?? getMeter("webhook-engine"); + + this.deliveryEnqueueCounter = this.meter.createCounter("webhook_delivery_enqueues_total", { + description: "Total number of webhook deliveries enqueued for routing", + }); + this.deliveryFilteredCounter = this.meter.createCounter("webhook_delivery_filtered_total", { + description: + "Total number of webhook deliveries received but not routed due to the endpoint filter", + }); + this.deliveryExecutionCounter = this.meter.createCounter("webhook_delivery_executions_total", { + description: "Total number of webhook delivery routing executions", + }); + this.deliveryExecutionDuration = this.meter.createHistogram( + "webhook_delivery_execution_duration_ms", + { description: "Duration of webhook delivery routing in milliseconds", unit: "ms" } + ); + this.deliveryExecutionFailureCounter = this.meter.createCounter( + "webhook_delivery_execution_failures_total", + { description: "Total number of webhook delivery routing failures" } + ); + this.ensurePartitionsCounter = this.meter.createCounter("webhook_ensure_partitions_total", { + description: "Total number of ensurePartitions cron runs", + }); + this.endpointCacheCounter = this.meter.createCounter("webhook_endpoint_cache_total", { + description: "Endpoint+secret cache lookups on the ingest hot path, by result (hit/miss)", + }); + + this.endpointCache = new TtlCache( + options.endpointCache?.ttlMs ?? 30_000, + options.endpointCache?.maxSize ?? 10_000 + ); + + this.worker = new Worker({ + name: "webhook-engine-worker", + redisOptions: { + ...options.redis, + keyPrefix: `${options.redis.keyPrefix ?? ""}webhook:`, + }, + catalog: { + ...webhookWorkerCatalog, + ensurePartitions: { + ...webhookWorkerCatalog.ensurePartitions, + cron: options.partitions?.ensureSchedule ?? webhookWorkerCatalog.ensurePartitions.cron, + jitterInMs: + options.partitions?.ensureJitterInMs ?? + webhookWorkerCatalog.ensurePartitions.jitterInMs, + }, + }, + concurrency: { + limit: options.worker.concurrency, + workers: options.worker.workers, + tasksPerWorker: options.worker.tasksPerWorker, + }, + pollIntervalMs: options.worker.pollIntervalMs, + shutdownTimeoutMs: options.worker.shutdownTimeoutMs, + logger: new Logger("WebhookEngineWorker", (options.logLevel ?? "info") as any), + jobs: { + "webhook.deliver": this.#handleDeliverJob.bind(this), + ensurePartitions: this.#handleEnsurePartitionsJob.bind(this), + }, + }); + + if (!options.worker.disabled) { + this.worker.start(); + this.logger.info("Webhook engine worker started", { + concurrency: options.worker.concurrency, + pollIntervalMs: options.worker.pollIntervalMs, + }); + } else { + this.logger.info("Webhook engine worker disabled"); + } + } + + // PUBLIC ENTRY: verify inline, append-only delivery write, enqueue routing, ack. + async ingest(input: IngestInput): Promise { + return startSpan(this.tracer, "webhook.ingest", async (span) => { + span.setAttribute("opaqueId", input.opaqueId); + + // 1+2. Resolve the endpoint + signing secret (cached per opaqueId). Fail-closed outcomes + // (not found / inactive / secret missing) are never cached, so verify only ever runs with a + // non-empty secret. + const resolved = await this.#resolveEndpoint(input.opaqueId); + if (!resolved.ok) return resolved.result; + const { endpoint, secret } = resolved; + + // 3. Verify inline. safeParse the Json artifact so a corrupt row is a 400, not a 5xx storm. + const parsedArtifact = WebhookVerifierArtifact.safeParse(endpoint.verifierArtifact); + if (!parsedArtifact.success) { + return { outcome: "verification_failed", error: "corrupt verifier artifact" }; + } + const verdict = verify(parsedArtifact.data, { + rawBytes: input.rawBytes, + headers: input.headers, + url: input.url, + secret, + }); + if (!verdict.ok) { + return { outcome: "verification_failed", error: verdict.error ?? "invalid" }; + } + + // Provider handshake (Slack url_verification, Discord PING): a signed request that must get a + // synchronous echo, not a recorded/routed delivery. Generic, declared on the verifier artifact. + const handshake = + "handshake" in parsedArtifact.data ? parsedArtifact.data.handshake : undefined; + if (handshake) { + const event = verdict.parsedEvent as unknown; + if (String(resolveBodyPath(event, handshake.matchPath) ?? "") === handshake.matchValue) { + return { + outcome: "handshake", + body: String(resolveBodyPath(event, handshake.respondPath) ?? ""), + }; + } + } + + return this.#recordAndRoute({ + endpoint, + artifact: parsedArtifact.data, + filterAst: resolved.filterAst, + parsedEvent: verdict.parsedEvent, + idempotencyKey: verdict.idempotencyKey, + errorMessage: verdict.error ?? null, + rawBytes: input.rawBytes, + headers: input.headers, + }); + }); + } + + /** + * Shared post-verify path for ingest() and simulateInject(): the atomic Redis front gate (dedupe), + * the filter gate, the append-only delivery row, and the routing enqueue. The front gate is + * two-phase (claim with a short lock via SET NX, then promote to the full dedupe window once the + * job is durably enqueued) so it is both atomic and crash-safe: a crash mid-ingest releases the + * short lock instead of suppressing the provider's retry, and the Run Engine idempotencyKey gate + * is the durable exactly-once guard. A filtered delivery is still recorded (for visibility) but not + * routed. + */ + async #recordAndRoute(args: { + endpoint: WebhookEndpoint; + artifact: WebhookVerifierArtifact; + filterAst: FilterAst | null; + parsedEvent: unknown; + idempotencyKey: string; + errorMessage: string | null; + rawBytes: Uint8Array; + headers: Record; + }): Promise { + const { + endpoint, + artifact, + filterAst, + parsedEvent, + idempotencyKey, + errorMessage, + rawBytes, + headers, + } = args; + + const secretHeader = + "config" in artifact && + artifact.config.scheme === "shared-secret" && + artifact.config.placement === "header" + ? artifact.config.fieldName + : undefined; + + const gateKey = `webhookdedupe:${endpoint.id}:${idempotencyKey}`; + const { id, friendlyId } = WebhookDeliveryId.generate(); + const createdAt = new Date(); + + const claimed = await this.frontGate.set( + gateKey, + id, + "EX", + this.#frontGateClaimTtlSeconds(), + "NX" + ); + if (claimed !== "OK") { + const existing = await this.frontGate.get(gateKey); + return { outcome: "duplicate", deliveryId: existing ?? id }; + } + + const { filtered, reason } = this.#evaluateFilter( + filterAst, + parsedEvent, + headers, + endpoint, + idempotencyKey + ); + + const isTest = Object.entries(headers).some( + ([key, value]) => key.toLowerCase() === "x-trigger-test" && Boolean(value) + ); + + try { + await this.prisma.webhookDelivery.create({ + data: { + id, + friendlyId, + createdAt, + webhookEndpointId: endpoint.id, + organizationId: endpoint.organizationId, + projectId: endpoint.projectId, + runtimeEnvironmentId: endpoint.runtimeEnvironmentId, + environmentType: endpoint.environmentType, + externalDeliveryId: idempotencyKey, + idempotencyKey, + status: filtered ? "FILTERED" : "PENDING", + isTest, + parsedEvent: capParsedEvent(parsedEvent), + headers: capHeaders(headers, secretHeader), + rawBodyHash: sha256Hex(rawBytes), + errorMessage, + filterReason: reason, + }, + }); + + if (filtered) { + this.deliveryFilteredCounter.add(1); + } else { + this.deliveryEnqueueCounter.add(1); + await this.worker.enqueueOnce({ + id: `webhook-delivery:${id}`, + job: "webhook.deliver", + payload: { deliveryId: id, createdAt }, + }); + } + } catch (error) { + await this.frontGate.del(gateKey).catch(() => {}); + return { outcome: "enqueue_failed", error: String(error) }; + } + + await this.frontGate + .set(gateKey, id, "EX", this.#frontGateTtlSeconds(artifact)) + .catch(() => {}); + + return { outcome: "accepted", deliveryId: id, deliveryFriendlyId: friendlyId }; + } + + /** + * Inject a delivery WITHOUT signature verification, then run the same filter + record + route path + * as ingest(). This is the test-console "simulate" mode for endpoints we cannot sign for + * (asymmetric public-key schemes; url-secret path placement). The body must be JSON. Everything + * downstream (filter, startOn, routing, run/session) runs for real. + */ + async simulateInject(input: IngestInput): Promise { + return startSpan(this.tracer, "webhook.simulate", async (span) => { + span.setAttribute("opaqueId", input.opaqueId); + + const endpoint = await this.prisma.webhookEndpoint.findFirst({ + where: { opaqueId: input.opaqueId }, + }); + if (!endpoint) return { outcome: "endpoint_not_found" }; + if (endpoint.status !== "ACTIVE") return { outcome: "endpoint_inactive" }; + + const parsedArtifact = WebhookVerifierArtifact.safeParse(endpoint.verifierArtifact); + if (!parsedArtifact.success) { + return { outcome: "verification_failed", error: "corrupt verifier artifact" }; + } + + const parsed = tryParseJson(input.rawBytes); + if (parsed.error || parsed.parsedEvent === undefined) { + return { outcome: "verification_failed", error: parsed.error ?? "body is not valid JSON" }; + } + + return this.#recordAndRoute({ + endpoint, + artifact: parsedArtifact.data, + filterAst: this.#parseStoredFilter(endpoint), + parsedEvent: parsed.parsedEvent, + idempotencyKey: deriveSimulateIdempotencyKey(parsedArtifact.data, input), + errorMessage: null, + rawBytes: input.rawBytes, + headers: input.headers, + }); + }); + } + + // Drop a cached endpoint+secret entry so a status change / secret rotation takes effect at once on + // this instance (other instances pick it up within the cache TTL). + invalidateEndpoint(opaqueId: string): void { + this.endpointCache.delete(opaqueId); + } + + // PUBLIC: re-run a past delivery's task from its stored event. We don't keep the raw body, so this + // re-triggers from the captured parsedEvent + headers (not a re-verify). A NEW delivery row with a + // fresh idempotency key is created so the run actually executes (not deduped) and the replay is + // auditable; it shares the original externalDeliveryId so the two group together. + async replayDelivery(input: { id: string; createdAt: Date }): Promise { + return startSpan(this.tracer, "webhook.replay", async (span) => { + span.setAttribute("deliveryId", input.id); + + const original = await this.prisma.webhookDelivery.findFirst({ + where: { id: input.id, createdAt: input.createdAt }, + }); + if (!original) return { outcome: "delivery_not_found" }; + + const endpoint = await this.prisma.webhookEndpoint.findFirst({ + where: { id: original.webhookEndpointId }, + }); + if (!endpoint) return { outcome: "endpoint_not_found" }; + if ((endpoint.routingTarget as WebhookRoutingTarget).type !== "task") { + return { outcome: "unsupported_target" }; + } + + const { id, friendlyId } = WebhookDeliveryId.generate(); + const createdAt = new Date(); + + await this.prisma.webhookDelivery.create({ + data: { + id, + friendlyId, + createdAt, + webhookEndpointId: original.webhookEndpointId, + organizationId: original.organizationId, + projectId: original.projectId, + runtimeEnvironmentId: original.runtimeEnvironmentId, + environmentType: original.environmentType, + externalDeliveryId: original.externalDeliveryId, // groups with the original + idempotencyKey: `replay:${id}`, // unique -> the Run Engine runs it, not dedupes + status: "PENDING", + parsedEvent: (original.parsedEvent ?? undefined) as Prisma.InputJsonValue | undefined, + headers: (original.headers ?? undefined) as Prisma.InputJsonValue | undefined, + rawBodyHash: original.rawBodyHash, + }, + }); + this.deliveryEnqueueCounter.add(1); + + await this.worker.enqueueOnce({ + id: `webhook-delivery:${id}`, + job: "webhook.deliver", + payload: { deliveryId: id, createdAt }, + }); + + return { outcome: "replayed", deliveryId: id, deliveryFriendlyId: friendlyId }; + }); + } + + // Resolve the endpoint + plaintext signing secret for an opaqueId, cached. Only fully-resolved + // (ACTIVE + secret present) endpoints are cached; fail-closed outcomes always re-read. + async #resolveEndpoint( + opaqueId: string + ): Promise< + | { ok: true; endpoint: WebhookEndpoint; secret: string; filterAst: FilterAst | null } + | { ok: false; result: IngestResult } + > { + const cached = this.endpointCache.get(opaqueId); + if (cached) { + this.endpointCacheCounter.add(1, { result: "hit" }); + return { + ok: true, + endpoint: cached.endpoint, + secret: cached.secret, + filterAst: cached.filterAst, + }; + } + this.endpointCacheCounter.add(1, { result: "miss" }); + + // Single-row via the global @unique on opaqueId (Q2). + const endpoint = await this.prisma.webhookEndpoint.findFirst({ where: { opaqueId } }); + if (!endpoint) return { ok: false, result: { outcome: "endpoint_not_found" } }; + if (endpoint.status !== "ACTIVE") + return { ok: false, result: { outcome: "endpoint_inactive" } }; + + // Injected port keeps the engine SecretStore-free. + const secret = endpoint.signingSecretKey + ? await this.options.resolveSigningSecret(endpoint.signingSecretKey) + : undefined; + if (!secret) return { ok: false, result: { outcome: "secret_missing" } }; + + // Parse the stored AST once per cache load. A corrupt AST fails open (route all) rather than + // blocking delivery — a filter bug must never silently swallow real webhooks. + const filterAst = this.#parseStoredFilter(endpoint); + + this.endpointCache.set(opaqueId, { endpoint, secret, filterAst }); + return { ok: true, endpoint, secret, filterAst }; + } + + #parseStoredFilter(endpoint: WebhookEndpoint): FilterAst | null { + if (endpoint.filterAst == null) return null; + const parsed = FilterAst.safeParse(endpoint.filterAst); + if (!parsed.success) { + this.logger.warn("webhook: corrupt filterAst, routing all", { endpointId: endpoint.id }); + return null; + } + return parsed.data; + } + + // Evaluate the endpoint filter (if any) against the verified delivery. Fail-open: an evaluation + // error routes the delivery (never drops a real webhook) and is logged. + // Evaluate a session routing target's `startOn` against the event. Absent => start allowed. A parse or + // eval error fails open (start allowed), matching the route filter, so a bad predicate never wedges a + // session. Parsed per delivery: only on the session path, and cheap relative to the DB + S2 work. + #evaluateSessionStart( + startOn: string | undefined, + delivery: WebhookDelivery, + endpoint: WebhookEndpoint + ): boolean { + if (!startOn) return true; + try { + const result = evaluateFilter(parseFilter(startOn), { + event: delivery.parsedEvent, + headers: (delivery.headers as Record | null) ?? {}, + webhook: { + externalRef: endpoint.endpointExternalRef, + tenantId: endpoint.endpointTenantId, + id: endpoint.handlerWebhookId, + source: endpoint.source, + deliveryId: delivery.externalDeliveryId, + }, + }); + return result.match; + } catch (error) { + this.logger.warn("webhook startOn evaluation failed, allowing start (fail-open)", { + endpointId: endpoint.id, + error: error instanceof Error ? error.message : String(error), + }); + return true; + } + } + + #evaluateFilter( + filterAst: FilterAst | null, + parsedEvent: unknown, + headers: Record, + endpoint: WebhookEndpoint, + deliveryId: string + ): { filtered: boolean; reason: string | null } { + if (!filterAst) return { filtered: false, reason: null }; + try { + const result = evaluateFilter(filterAst, { + event: parsedEvent, + headers, + webhook: { + externalRef: endpoint.endpointExternalRef, + tenantId: endpoint.endpointTenantId, + id: endpoint.handlerWebhookId, + source: endpoint.source, + deliveryId, + }, + }); + return result.match + ? { filtered: false, reason: null } + : { filtered: true, reason: result.reason ?? null }; + } catch (error) { + this.logger.warn("webhook filter evaluation failed, routing (fail-open)", { + endpointId: endpoint.id, + error: error instanceof Error ? error.message : String(error), + }); + return { filtered: false, reason: null }; + } + } + + // Best-effort dedupe window. Capped at the configured max; the durable Run Engine + // idempotencyKey gate is the real guard for late retries past this window. + #frontGateTtlSeconds(_artifact: WebhookVerifierArtifact): number { + const def = this.options.frontGate?.defaultTtlSeconds ?? 6 * 60 * 60; + const max = this.options.frontGate?.maxTtlSeconds ?? 6 * 60 * 60; + return Math.min(def, max); + } + + // Short "processing lock" TTL held between the atomic claim and the promote-to-full-window. Keep + // it well above the create+enqueue latency (ms) but short enough that a crash mid-ingest releases + // the key long before a provider's retry, so events are re-processed rather than dropped. + #frontGateClaimTtlSeconds(): number { + return this.options.frontGate?.claimTtlSeconds ?? 60; + } + + // webhook.deliver handler: load delivery + endpoint, route to the task via the + // injected port, mark terminal. Run Engine idempotency is the durable dedupe gate. + async #handleDeliverJob({ + payload, + attempt, + }: JobHandlerParams) { + return startSpan(this.tracer, "webhook.deliver", async (span) => { + span.setAttribute("deliveryId", payload.deliveryId); + this.deliveryExecutionCounter.add(1); + const start = performance.now(); + + // 1. Load by composite PK. (id, createdAt) prunes to exactly one partition. + const delivery = await this.prisma.webhookDelivery.findFirst({ + where: { id: payload.deliveryId, createdAt: payload.createdAt }, + }); + if (!delivery) { + this.logger.error("webhook.deliver: delivery not found", { + deliveryId: payload.deliveryId, + }); + return; // terminal; nothing to retry + } + + if (delivery.status === "SUCCEEDED" || delivery.status === "FAILED") { + this.logger.debug("webhook.deliver: already terminal", { + deliveryId: delivery.id, + status: delivery.status, + }); + return; + } + + const endpoint = await this.prisma.webhookEndpoint.findFirst({ + where: { id: delivery.webhookEndpointId }, + }); + if (!endpoint) { + await this.#markFailed(delivery, "Endpoint not found"); + return; + } + + // 2. Mark PROCESSING (best-effort). + await this.prisma.webhookDelivery.update({ + where: { id_createdAt: { id: delivery.id, createdAt: delivery.createdAt } }, + data: { status: "PROCESSING" }, + }); + + // 3. Resolve route. + const routingTarget = endpoint.routingTarget as WebhookRoutingTarget; + + if (routingTarget.type === "session") { + await this.#deliverToSession(delivery, endpoint, routingTarget, start, attempt); + return; + } + if (routingTarget.type !== "task") { + await this.#markFailed( + delivery, + `Unsupported routing target: ${(routingTarget as { type: string }).type}` + ); + return; + } + + // 4. Call the injected triggerTask port. + const identityTags = [ + `webhook:endpoint:${endpoint.id}`, + `webhook:source:${endpoint.source}`, + ...(endpoint.endpointTenantId ? [`webhook:tenant:${endpoint.endpointTenantId}`] : []), + ]; + + const result = await this.options.triggerTask({ + environmentId: delivery.runtimeEnvironmentId, + taskId: routingTarget.taskId, + idempotencyKey: delivery.idempotencyKey, // = externalDeliveryId + idempotencyKeyExpiresAt: this.#retryWindowExpiry(endpoint), + payload: delivery.parsedEvent, + headers: (delivery.headers as Record | null) ?? {}, + identityTags, + endpointMetadata: endpoint.metadata, + }); + + await this.#applyDeliverResult(delivery, result, start, attempt); + }); + } + + // Route a verified delivery to a session: resolve the key template to the session externalId, then + // hand off to the injected deliverToSession port (find-or-create the session, append the action). + async #deliverToSession( + delivery: WebhookDelivery, + endpoint: WebhookEndpoint, + routingTarget: Extract, + start: number, + attempt: number + ) { + if (!this.options.deliverToSession) { + await this.#markFailed(delivery, "Session delivery is not configured"); + return; + } + + const externalId = evaluateSessionKeyTemplate(routingTarget.keyTemplate, { + body: delivery.parsedEvent, + webhook: { + externalRef: endpoint.endpointExternalRef ?? "", + tenantId: endpoint.endpointTenantId ?? "", + id: endpoint.handlerWebhookId, + source: endpoint.source, + deliveryId: delivery.externalDeliveryId, + }, + header: (delivery.headers as Record | null) ?? {}, + }); + if (!externalId) { + await this.#markFailed(delivery, `Session key resolved empty: ${routingTarget.keyTemplate}`); + return; + } + + const isSessionStart = this.#evaluateSessionStart(routingTarget.startOn, delivery, endpoint); + + const result = await this.options.deliverToSession({ + environmentId: delivery.runtimeEnvironmentId, + taskIdentifier: routingTarget.taskIdentifier, + externalId, + deliverAs: routingTarget.deliverAs, + actionType: routingTarget.actionType, + connectorId: routingTarget.connectorId, + event: delivery.parsedEvent, + source: endpoint.source, + headers: (delivery.headers as Record | null) ?? {}, + deliveryId: delivery.externalDeliveryId, + triggerConfigTemplate: routingTarget.triggerConfigTemplate, + idempotencyKey: delivery.idempotencyKey, + isSessionStart, + }); + + await this.#applyDeliverResult(delivery, result, start, attempt); + } + + // Mark a delivery terminal from a port result. Shared by the task and session paths: success -> + // SUCCEEDED (+runId); a transient error resets to PENDING and throws so redis-worker retries, EXCEPT + // on the final attempt where it's marked FAILED (throwing there would DLQ the job and leave the + // delivery stuck PENDING); any other error -> FAILED. + async #applyDeliverResult( + delivery: WebhookDelivery, + result: { + success: boolean; + runId?: string; + error?: string; + errorType?: WebhookDeliverTaskErrorType; + skipped?: boolean; + skippedReason?: string; + }, + start: number, + attempt: number + ) { + this.deliveryExecutionDuration.record(performance.now() - start); + + if (result.skipped) { + // Resume-only and no session existed: terminal + visible, but nothing routed (no run, no egress). + await this.prisma.webhookDelivery.update({ + where: { id_createdAt: { id: delivery.id, createdAt: delivery.createdAt } }, + data: { + status: "FILTERED", + filterReason: result.skippedReason ?? "startOn: not a session-start event", + processedAt: new Date(), + }, + }); + } else if (result.success) { + await this.prisma.webhookDelivery.update({ + where: { id_createdAt: { id: delivery.id, createdAt: delivery.createdAt } }, + data: { status: "SUCCEEDED", runId: result.runId ?? null, processedAt: new Date() }, + }); + } else if (result.errorType === "QUEUE_LIMIT" || result.errorType === "SYSTEM_ERROR") { + if (attempt >= WEBHOOK_DELIVER_MAX_ATTEMPTS - 1) { + await this.#markFailed( + delivery, + result.error ?? "webhook.deliver failed after max attempts" + ); + return; + } + await this.prisma.webhookDelivery.update({ + where: { id_createdAt: { id: delivery.id, createdAt: delivery.createdAt } }, + data: { status: "PENDING", errorMessage: result.error ?? null }, + }); + this.deliveryExecutionFailureCounter.add(1); + throw new Error(result.error ?? "webhook.deliver transient failure"); + } else { + await this.#markFailed(delivery, result.error ?? "webhook.deliver failed"); + } + } + + async #markFailed(delivery: WebhookDelivery, message: string) { + this.deliveryExecutionFailureCounter.add(1); + await this.prisma.webhookDelivery.update({ + where: { id_createdAt: { id: delivery.id, createdAt: delivery.createdAt } }, + data: { status: "FAILED", errorMessage: message, processedAt: new Date() }, + }); + } + + #retryWindowExpiry(endpoint: WebhookEndpoint): Date { + return new Date(Date.now() + resolveRetryWindowSeconds(endpoint) * 1000); + } + + // ensurePartitions cron handler. Pre-creates dated partitions ahead + drops cold ones. + async #handleEnsurePartitionsJob( + _job: JobHandlerParams + ) { + return startSpan(this.tracer, "ensurePartitions", async (span) => { + this.ensurePartitionsCounter.add(1); + const result = await ensurePartitions(this.prisma, { + now: new Date(), + lookaheadDays: this.options.partitions?.lookaheadDays ?? 10, // 7..14 + retentionDays: this.options.partitions?.retentionDays ?? 7, + }); + span.setAttribute("created", result.created.length); + span.setAttribute("dropped", result.dropped.length); + span.setAttribute("deferred", result.deferred.length); + this.logger.info("webhook ensurePartitions", result); + }); + } + + async getJob(id: string) { + return this.worker.getJob(id); + } + + async quit() { + this.logger.info("Shutting down webhook engine"); + + try { + await this.worker.stop(); + await this.frontGate.quit(); + this.logger.info("Webhook engine worker stopped successfully"); + } catch (error) { + this.logger.error("Error stopping webhook engine worker", { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + } +} + +// The provider retry window: idempotencyKeyExpiresAt = now + this. Must exceed the +// longest provider retry horizon so a late retry still hits the durable gate. +function resolveRetryWindowSeconds(endpoint: { source: string }): number { + const DAY = 24 * 60 * 60; + switch (endpoint.source) { + case "stripe": + return 3 * DAY; + case "github": + return 1 * DAY; + default: + return 1 * DAY; + } +} + +const DROP_HEADERS = new Set(["authorization", "proxy-authorization", "cookie", "set-cookie"]); +const HEADERS_CAP_BYTES = 4 * 1024; + +// Curate the headers stored on the delivery row (and thus surfaced to onEvent): drop credential +// headers + any scheme secret-bearing header, and bound the total size. +function capHeaders(headers: Record, secretHeader?: string): Prisma.InputJsonValue { + const drop = secretHeader ? new Set([...DROP_HEADERS, secretHeader.toLowerCase()]) : DROP_HEADERS; + const out: Record = {}; + let bytes = 0; + for (const [key, value] of Object.entries(headers)) { + if (drop.has(key.toLowerCase())) continue; + const entryBytes = key.length + (typeof value === "string" ? value.length : 0) + 4; + if (bytes + entryBytes > HEADERS_CAP_BYTES) continue; + out[key] = value; + bytes += entryBytes; + } + return out; +} + +function deriveSimulateIdempotencyKey( + artifact: WebhookVerifierArtifact, + input: IngestInput +): string { + const idempotencyField = "config" in artifact ? artifact.config.idempotencyField : undefined; + const lowerHeaders: Record = {}; + for (const [key, value] of Object.entries(input.headers)) lowerHeaders[key.toLowerCase()] = value; + return deriveIdempotencyKey({ + idempotencyField, + headers: lowerHeaders, + rawBytes: input.rawBytes, + timestampValue: "", + signatureValue: "simulate", + }); +} + +// Size-cap the verified event snapshot stored on the row (the full event lives in +// ClickHouse). Past the cap, store a marker instead of the payload. +function capParsedEvent(event: unknown): Prisma.InputJsonValue | undefined { + if (event === undefined || event === null) return undefined; + try { + const serialized = JSON.stringify(event); + const CAP_BYTES = 8 * 1024; + if (serialized.length <= CAP_BYTES) return event as Prisma.InputJsonValue; + return { truncated: true, bytes: serialized.length } as Prisma.InputJsonValue; + } catch { + return undefined; + } +} diff --git a/internal-packages/webhook-engine/src/engine/ingest.test.ts b/internal-packages/webhook-engine/src/engine/ingest.test.ts new file mode 100644 index 00000000000..37178543635 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/ingest.test.ts @@ -0,0 +1,772 @@ +import { createHmac, randomBytes } from "node:crypto"; +import { + containerTestWithIsolatedRedisNoClickhouse, + createStandalonePostgresContainer, +} from "@internal/testcontainers"; +import { createRedisClient } from "@internal/redis"; +import { type Prisma, PrismaClient } from "@trigger.dev/database"; +import type { RedisOptions } from "@internal/redis"; +import { WebhookEndpointId } from "@trigger.dev/core/v3/isomorphic"; +import { expect } from "vitest"; +import { WebhookEngine } from "./index.js"; +import { parseFilter } from "./filter/index.js"; +import type { + DeliverWebhookToSessionCallback, + DeliverWebhookToSessionParams, + TriggerWebhookTaskCallback, + TriggerWebhookTaskParams, +} from "./types.js"; + +// End-to-end ingest -> deliver over a real Postgres + Redis: the de-risking test for the atomic +// front gate (happy path, concurrent dedup, crash recovery) and the Run-Engine idempotencyKey wiring. + +const SECRET = "whsec_integration_secret"; +const SECRET_KEY = "secretref_integration"; + +const VERIFIER_CONFIG = { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { source: { from: "signatureField", field: "t" }, toleranceSeconds: 300 }, + signingString: { template: "{timestamp}.{body}" }, + idempotencyField: { from: "body", name: "id" }, +} as const; + +// Live clock so the 300s tolerance passes (the engine calls verify() without an injected clock). +function signedInput(eventId: string, opaqueId: string, type = "payment_intent.succeeded") { + const body = JSON.stringify({ id: eventId, type }); + const t = Math.floor(Date.now() / 1000); + const sig = createHmac("sha256", SECRET).update(`${t}.${body}`).digest("hex"); + return { + opaqueId, + rawBytes: new TextEncoder().encode(body), + headers: { "stripe-signature": `t=${t},v1=${sig}` }, + url: `https://api.example.com/webhooks/v1/ingest/${opaqueId}`, + }; +} + +async function createEndpoint(prisma: PrismaClient, over?: { filter?: string }) { + return prisma.webhookEndpoint.create({ + data: { + friendlyId: WebhookEndpointId.generate().friendlyId, + opaqueId: `op_${randomBytes(12).toString("hex")}`, + organizationId: "org_test", + projectId: "proj_test", + runtimeEnvironmentId: "env_test", + environmentType: "PRODUCTION", + source: "stripe", + handlerWebhookId: "handle-stripe-webhook", + routingTarget: { type: "task", taskId: "handle-stripe-task" }, + verifierArtifact: { kind: "config", config: VERIFIER_CONFIG }, + signingSecretKey: SECRET_KEY, + status: "ACTIVE", + ...(over?.filter + ? { + filter: over.filter, + filterAst: parseFilter(over.filter) as unknown as Prisma.InputJsonValue, + filterAstVersion: 1, + } + : {}), + }, + }); +} + +// Models Run-Engine idempotency: same idempotencyKey -> same run, so distinct runs = runsByKey.size. +function makeTriggerTaskStub() { + const calls: TriggerWebhookTaskParams[] = []; + const runsByKey = new Map(); + let runCounter = 0; + const triggerTask: TriggerWebhookTaskCallback = async (params) => { + calls.push(params); + let runId = runsByKey.get(params.idempotencyKey); + if (!runId) { + runId = `run_${++runCounter}`; + runsByKey.set(params.idempotencyKey, runId); + } + return { success: true, runId }; + }; + return { triggerTask, calls, runsByKey }; +} + +// Session-routing endpoint: routes to a find-or-created session (keyTemplate -> externalId) instead +// of a task. The stub stands in for the webapp's deliverToSession port (find-or-create + append). +async function createSessionEndpoint( + prisma: PrismaClient, + keyTemplate: string, + startOn?: string, + handlerWebhookId = "agent-x:orders" +) { + return prisma.webhookEndpoint.create({ + data: { + friendlyId: WebhookEndpointId.generate().friendlyId, + opaqueId: `op_${randomBytes(12).toString("hex")}`, + organizationId: "org_test", + projectId: "proj_test", + runtimeEnvironmentId: "env_test", + environmentType: "PRODUCTION", + source: "stripe", + handlerWebhookId, + routingTarget: { + type: "session", + taskIdentifier: "agent-x", + keyTemplate, + actionType: "order.event", + deliverAs: "action", + ...(startOn ? { startOn } : {}), + }, + verifierArtifact: { kind: "config", config: VERIFIER_CONFIG }, + signingSecretKey: SECRET_KEY, + status: "ACTIVE", + }, + }); +} + +function makeDeliverToSessionStub() { + const calls: DeliverWebhookToSessionParams[] = []; + const runsByExternalId = new Map(); + let runCounter = 0; + const deliverToSession: DeliverWebhookToSessionCallback = async (params) => { + calls.push(params); + let runId = runsByExternalId.get(params.externalId); + if (!runId) { + runId = `srun_${++runCounter}`; + runsByExternalId.set(params.externalId, runId); + } + return { success: true, runId }; + }; + return { deliverToSession, calls, runsByExternalId }; +} + +function buildEngine( + prisma: PrismaClient, + redisOptions: RedisOptions, + triggerTask: TriggerWebhookTaskCallback, + over?: { + claimTtlSeconds?: number; + workerDisabled?: boolean; + endpointCacheTtlMs?: number; + resolveSigningSecret?: (key: string) => Promise; + deliverToSession?: DeliverWebhookToSessionCallback; + } +) { + return new WebhookEngine({ + prisma, + redis: redisOptions, + worker: { concurrency: 1, pollIntervalMs: 50, disabled: over?.workerDisabled }, + frontGate: over?.claimTtlSeconds ? { claimTtlSeconds: over.claimTtlSeconds } : undefined, + endpointCache: + over?.endpointCacheTtlMs !== undefined ? { ttlMs: over.endpointCacheTtlMs } : undefined, + triggerTask, + deliverToSession: over?.deliverToSession, + resolveSigningSecret: + over?.resolveSigningSecret ?? (async (key) => (key === SECRET_KEY ? SECRET : undefined)), + logLevel: "error", + }); +} + +async function waitFor(fn: () => Promise, timeoutMs = 10_000, intervalMs = 50) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (await fn()) return; + await new Promise((r) => setTimeout(r, intervalMs)); + } + throw new Error(`waitFor timed out after ${timeoutMs}ms`); +} + +containerTestWithIsolatedRedisNoClickhouse( + "ingest -> deliver routes a verified event to exactly one run", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + const { triggerTask, calls, runsByKey } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + const eventId = "evt_happy_1"; + const result = await engine.ingest(signedInput(eventId, endpoint.opaqueId)); + + expect(result.outcome).toBe("accepted"); + if (result.outcome !== "accepted") return; + const deliveryId = result.deliveryId; + + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: deliveryId } }); + return d?.status === "SUCCEEDED"; + }); + + const delivery = await prisma.webhookDelivery.findFirst({ where: { id: deliveryId } }); + expect(delivery?.runId).toBe("run_1"); + expect(delivery?.externalDeliveryId).toBe(eventId); + + expect(calls).toHaveLength(1); + expect(runsByKey.size).toBe(1); + expect(calls[0].taskId).toBe("handle-stripe-task"); + expect(calls[0].idempotencyKey).toBe(eventId); + } finally { + await engine.quit(); + } + } +); + +// Two-database mode: point the engine at a SEPARATE Postgres (the future data-plane DB) with the +// fixture's `prisma` as the main DB, and prove webhook writes land only on the injected client (a +// leaked query onto the main client would show up as a row there). Runtime backstop to the types. +containerTestWithIsolatedRedisNoClickhouse( + "two-database mode: webhook writes land on the injected client's DB, never the main DB", + async ({ prisma, redisOptions }) => { + const { container: webhookContainer, url: webhookUrl } = + await createStandalonePostgresContainer(); + const webhookDb = new PrismaClient({ datasources: { db: { url: webhookUrl } } }); + const { triggerTask } = makeTriggerTaskStub(); + const engine = buildEngine(webhookDb, redisOptions, triggerTask); + + try { + // The endpoint lives on the webhook DB; the engine reads it from its own client. + const endpoint = await createEndpoint(webhookDb); + + const eventId = "evt_two_db_1"; + const result = await engine.ingest(signedInput(eventId, endpoint.opaqueId)); + expect(result.outcome).toBe("accepted"); + if (result.outcome !== "accepted") return; + + await waitFor(async () => { + const d = await webhookDb.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + return d?.status === "SUCCEEDED"; + }); + + // The delivery + endpoint are on the webhook DB... + expect(await webhookDb.webhookDelivery.count()).toBe(1); + expect(await webhookDb.webhookEndpoint.count()).toBe(1); + + // ...and nothing leaked onto the main DB. + expect(await prisma.webhookDelivery.count()).toBe(0); + expect(await prisma.webhookEndpoint.count()).toBe(0); + } finally { + await engine.quit(); + await webhookDb.$disconnect(); + await webhookContainer.stop(); + } + }, + 120_000 +); + +containerTestWithIsolatedRedisNoClickhouse( + "a provider handshake (url_verification) echoes the challenge and records no delivery", + async ({ prisma, redisOptions }) => { + const endpoint = await prisma.webhookEndpoint.create({ + data: { + friendlyId: WebhookEndpointId.generate().friendlyId, + opaqueId: `op_${randomBytes(12).toString("hex")}`, + organizationId: "org_test", + projectId: "proj_test", + runtimeEnvironmentId: "env_test", + environmentType: "PRODUCTION", + source: "slack", + handlerWebhookId: "handle-slack-channel", + routingTarget: { type: "task", taskId: "unused" }, + verifierArtifact: { + kind: "config", + config: VERIFIER_CONFIG, + handshake: { + matchPath: "type", + matchValue: "url_verification", + respondPath: "challenge", + }, + }, + signingSecretKey: SECRET_KEY, + status: "ACTIVE", + }, + }); + const { triggerTask } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + const body = JSON.stringify({ type: "url_verification", challenge: "chal_xyz" }); + const t = Math.floor(Date.now() / 1000); + const sig = createHmac("sha256", SECRET).update(`${t}.${body}`).digest("hex"); + const result = await engine.ingest({ + opaqueId: endpoint.opaqueId, + rawBytes: new TextEncoder().encode(body), + headers: { "stripe-signature": `t=${t},v1=${sig}` }, + url: `https://api.example.com/webhooks/v1/ingest/${endpoint.opaqueId}`, + }); + + expect(result.outcome).toBe("handshake"); + if (result.outcome === "handshake") expect(result.body).toBe("chal_xyz"); + + const count = await prisma.webhookDelivery.count({ + where: { webhookEndpointId: endpoint.id }, + }); + expect(count).toBe(0); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "concurrent duplicate deliveries collapse to one delivery row and one run", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + const { triggerTask, calls, runsByKey } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + const eventId = "evt_dupe_1"; + const input = signedInput(eventId, endpoint.opaqueId); + + const [a, b] = await Promise.all([engine.ingest(input), engine.ingest(input)]); + + // The atomic SET NX claim guarantees exactly one accepted and one duplicate. + expect([a.outcome, b.outcome].sort()).toEqual(["accepted", "duplicate"]); + + const accepted = (a.outcome === "accepted" ? a : b) as Extract< + typeof a, + { outcome: "accepted" } + >; + const duplicate = (a.outcome === "duplicate" ? a : b) as Extract< + typeof a, + { outcome: "duplicate" } + >; + expect(duplicate.deliveryId).toBe(accepted.deliveryId); + + const rows = await prisma.webhookDelivery.findMany({ + where: { webhookEndpointId: endpoint.id }, + }); + expect(rows).toHaveLength(1); + + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: accepted.deliveryId } }); + return d?.status === "SUCCEEDED"; + }); + + expect(calls).toHaveLength(1); + expect(runsByKey.size).toBe(1); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "stores curated, size-bounded headers (drops credentials and oversized headers)", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + const { triggerTask } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask, { workerDisabled: true }); + + try { + const input = signedInput("evt_headers_1", endpoint.opaqueId); + input.headers["authorization"] = "Bearer super-secret"; + input.headers["cookie"] = "session=abc"; + input.headers["x-custom"] = "keep-me"; + input.headers["x-huge"] = "z".repeat(8 * 1024); + + const result = await engine.ingest(input); + expect(result.outcome).toBe("accepted"); + if (result.outcome !== "accepted") return; + + const delivery = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + const stored = delivery!.headers as Record; + + expect(stored["authorization"]).toBeUndefined(); + expect(stored["cookie"]).toBeUndefined(); + expect(stored["x-huge"]).toBeUndefined(); + expect(stored["x-custom"]).toBe("keep-me"); + expect(stored["stripe-signature"]).toBeDefined(); + expect(JSON.stringify(stored).length).toBeLessThanOrEqual(4 * 1024 + 256); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "caches the endpoint + secret across ingests and re-reads after the TTL", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + const { triggerTask } = makeTriggerTaskStub(); + let secretReads = 0; + const engine = buildEngine(prisma, redisOptions, triggerTask, { + endpointCacheTtlMs: 200, + resolveSigningSecret: async (key) => { + secretReads++; + return key === SECRET_KEY ? SECRET : undefined; + }, + }); + + try { + expect((await engine.ingest(signedInput("evt_cache_1", endpoint.opaqueId))).outcome).toBe( + "accepted" + ); + expect((await engine.ingest(signedInput("evt_cache_2", endpoint.opaqueId))).outcome).toBe( + "accepted" + ); + // Second ingest hit the cache, so the secret was resolved only once. + expect(secretReads).toBe(1); + + // After the TTL the entry expires and the next ingest re-resolves. + await new Promise((r) => setTimeout(r, 250)); + expect((await engine.ingest(signedInput("evt_cache_3", endpoint.opaqueId))).outcome).toBe( + "accepted" + ); + expect(secretReads).toBe(2); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "a crash between claim and promote releases the short lock so the event is re-processed", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + const { triggerTask } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask, { + claimTtlSeconds: 1, + workerDisabled: true, + }); + const gate = createRedisClient(redisOptions); + + try { + const eventId = "evt_crash_1"; + const input = signedInput(eventId, endpoint.opaqueId); + const gateKey = `webhookdedupe:${endpoint.id}:${eventId}`; + + // Post-crash state: claim set (short TTL), but the ingest died before creating the row. + await gate.set(gateKey, "whd_orphaned_claim", "EX", 1, "NX"); + + const blocked = await engine.ingest(input); + expect(blocked.outcome).toBe("duplicate"); + expect( + await prisma.webhookDelivery.count({ where: { webhookEndpointId: endpoint.id } }) + ).toBe(0); + + // Once the short lock expires, a retry re-claims and the event is processed, not dropped. + await waitFor(async () => (await gate.get(gateKey)) === null, 5_000); + + const recovered = await engine.ingest(input); + expect(recovered.outcome).toBe("accepted"); + expect( + await prisma.webhookDelivery.count({ where: { webhookEndpointId: endpoint.id } }) + ).toBe(1); + } finally { + await gate.quit(); + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "filter gate: a non-matching event is recorded FILTERED and not routed; a match is routed", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma, { + filter: "event.type == 'payment_intent.succeeded'", + }); + const { triggerTask, calls } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + // Matching event -> routed to a run as usual. + const match = await engine.ingest(signedInput("evt_match", endpoint.opaqueId)); + expect(match.outcome).toBe("accepted"); + if (match.outcome !== "accepted") return; + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: match.deliveryId } }); + return d?.status === "SUCCEEDED"; + }); + expect(calls).toHaveLength(1); + + // Non-matching event -> still received (200 + row), recorded FILTERED, never routed. + const filtered = await engine.ingest( + signedInput("evt_nomatch", endpoint.opaqueId, "payment_intent.failed") + ); + expect(filtered.outcome).toBe("accepted"); + if (filtered.outcome !== "accepted") return; + + const row = await prisma.webhookDelivery.findFirst({ where: { id: filtered.deliveryId } }); + expect(row?.status).toBe("FILTERED"); + expect(row?.runId).toBeNull(); + expect(row?.filterReason).toContain("event.type"); + + // The deliver worker must never pick it up: no new triggerTask call after a beat. + await new Promise((r) => setTimeout(r, 300)); + expect(calls).toHaveLength(1); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "session routing resolves the key and delivers to the session port", + async ({ prisma, redisOptions }) => { + const endpoint = await createSessionEndpoint(prisma, "{body.id}"); + const { triggerTask } = makeTriggerTaskStub(); + const { deliverToSession, calls } = makeDeliverToSessionStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask, { deliverToSession }); + + try { + const eventId = "evt_sess_1"; + const result = await engine.ingest(signedInput(eventId, endpoint.opaqueId)); + if (result.outcome !== "accepted") + throw new Error(`expected accepted, got ${result.outcome}`); + + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + return d?.status === "SUCCEEDED"; + }); + + expect(calls).toHaveLength(1); + expect(calls[0]?.externalId).toBe(eventId); // {body.id} -> event id + expect(calls[0]?.taskIdentifier).toBe("agent-x"); + expect(calls[0]?.actionType).toBe("order.event"); + expect((calls[0]?.event as { id: string }).id).toBe(eventId); + expect(calls[0]?.deliveryId).toBe(eventId); + + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + expect(d?.runId).toBe("srun_1"); // the session's run, from the port + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "session delivery with an unresolvable key is FAILED and never calls the port", + async ({ prisma, redisOptions }) => { + const endpoint = await createSessionEndpoint(prisma, "{body.customerId}"); // not in the body + const { triggerTask } = makeTriggerTaskStub(); + const { deliverToSession, calls } = makeDeliverToSessionStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask, { deliverToSession }); + + try { + const result = await engine.ingest(signedInput("evt_sess_nokey", endpoint.opaqueId)); + if (result.outcome !== "accepted") + throw new Error(`expected accepted, got ${result.outcome}`); + + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + return d?.status === "FAILED"; + }); + + expect(calls).toHaveLength(0); + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + expect(d?.errorMessage).toContain("key resolved empty"); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "startOn is evaluated and passed to the port as isSessionStart (default true when absent)", + async ({ prisma, redisOptions }) => { + const gated = await createSessionEndpoint(prisma, "{body.id}", "event.type == 'session.start'"); + const open = await createSessionEndpoint(prisma, "{body.id}", undefined, "agent-y:orders"); // no startOn + const { triggerTask } = makeTriggerTaskStub(); + const { deliverToSession, calls } = makeDeliverToSessionStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask, { deliverToSession }); + + try { + const rStart = await engine.ingest(signedInput("evt_start", gated.opaqueId, "session.start")); + const rOther = await engine.ingest(signedInput("evt_other", gated.opaqueId, "message")); + const rOpen = await engine.ingest(signedInput("evt_open", open.opaqueId, "message")); + for (const r of [rStart, rOther, rOpen]) { + if (r.outcome !== "accepted") throw new Error(`expected accepted, got ${r.outcome}`); + } + + await waitFor(async () => calls.length === 3); + const byId = (id: string) => calls.find((c) => (c.event as { id: string }).id === id); + expect(byId("evt_start")?.isSessionStart).toBe(true); // matches startOn + expect(byId("evt_other")?.isSessionStart).toBe(false); // does not match startOn + expect(byId("evt_open")?.isSessionStart).toBe(true); // no startOn -> default true + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "a skipped session result is recorded FILTERED with the reason and no run", + async ({ prisma, redisOptions }) => { + const endpoint = await createSessionEndpoint( + prisma, + "{body.id}", + "event.type == 'session.start'" + ); + const { triggerTask } = makeTriggerTaskStub(); + // Stand in for the webapp: resume-only (no session yet) + not a start event -> skipped. + const calls: DeliverWebhookToSessionParams[] = []; + const deliverToSession: DeliverWebhookToSessionCallback = async (params) => { + calls.push(params); + if (!params.isSessionStart) { + return { + success: true, + skipped: true, + skippedReason: "startOn: not a session-start event", + }; + } + return { success: true, runId: "srun_start" }; + }; + const engine = buildEngine(prisma, redisOptions, triggerTask, { deliverToSession }); + + try { + const skip = await engine.ingest(signedInput("evt_skip", endpoint.opaqueId, "message")); + const start = await engine.ingest(signedInput("evt_go", endpoint.opaqueId, "session.start")); + if (skip.outcome !== "accepted" || start.outcome !== "accepted") + throw new Error("expected accepted"); + + await waitFor(async () => { + const a = await prisma.webhookDelivery.findFirst({ where: { id: skip.deliveryId } }); + const b = await prisma.webhookDelivery.findFirst({ where: { id: start.deliveryId } }); + return a?.status === "FILTERED" && b?.status === "SUCCEEDED"; + }); + + const skipped = await prisma.webhookDelivery.findFirst({ where: { id: skip.deliveryId } }); + expect(skipped?.status).toBe("FILTERED"); + expect(skipped?.filterReason).toContain("startOn"); + expect(skipped?.runId).toBeNull(); + + const started = await prisma.webhookDelivery.findFirst({ where: { id: start.deliveryId } }); + expect(started?.runId).toBe("srun_start"); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "session delivery fails when the deliverToSession port is not configured", + async ({ prisma, redisOptions }) => { + const endpoint = await createSessionEndpoint(prisma, "{body.id}"); + const { triggerTask } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); // no deliverToSession port + + try { + const result = await engine.ingest(signedInput("evt_sess_noport", endpoint.opaqueId)); + if (result.outcome !== "accepted") + throw new Error(`expected accepted, got ${result.outcome}`); + + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + return d?.status === "FAILED"; + }); + + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + expect(d?.errorMessage).toContain("not configured"); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "a delivery whose port keeps failing transiently is FAILED after max attempts (not stuck PENDING)", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + let calls = 0; + const triggerTask: TriggerWebhookTaskCallback = async () => { + calls++; + return { success: false, errorType: "SYSTEM_ERROR", error: "boom" }; + }; + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + const result = await engine.ingest(signedInput("evt_exhaust", endpoint.opaqueId)); + if (result.outcome !== "accepted") + throw new Error(`expected accepted, got ${result.outcome}`); + + // 5 attempts with backoff (~1+2+4+8s), then the final attempt marks FAILED instead of PENDING. + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + return d?.status === "FAILED"; + }, 40_000); + + const d = await prisma.webhookDelivery.findFirst({ where: { id: result.deliveryId } }); + expect(d?.status).toBe("FAILED"); + expect(d?.errorMessage).toContain("boom"); + expect(calls).toBe(5); // maxAttempts; not left retrying / stuck PENDING + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "simulateInject skips verification: an unsigned body that ingest() rejects is still recorded + routed", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma); + const { triggerTask, calls, runsByKey } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + const eventId = "evt_simulate_1"; + const body = JSON.stringify({ id: eventId, type: "payment_intent.succeeded" }); + const unsignedInput = { + opaqueId: endpoint.opaqueId, + rawBytes: new TextEncoder().encode(body), + headers: {}, + url: `https://api.example.com/webhooks/v1/ingest/${endpoint.opaqueId}`, + }; + + const rejected = await engine.ingest(unsignedInput); + expect(rejected.outcome).toBe("verification_failed"); + + const simulated = await engine.simulateInject(unsignedInput); + expect(simulated.outcome).toBe("accepted"); + if (simulated.outcome !== "accepted") return; + + await waitFor(async () => { + const d = await prisma.webhookDelivery.findFirst({ where: { id: simulated.deliveryId } }); + return d?.status === "SUCCEEDED"; + }); + + const delivery = await prisma.webhookDelivery.findFirst({ + where: { id: simulated.deliveryId }, + }); + expect(delivery?.runId).toBe("run_1"); + expect(delivery?.externalDeliveryId).toBe(eventId); + expect(delivery?.errorMessage).toBeNull(); + + expect(calls).toHaveLength(1); + expect(runsByKey.size).toBe(1); + expect(calls[0].taskId).toBe("handle-stripe-task"); + expect(calls[0].idempotencyKey).toBe(eventId); + } finally { + await engine.quit(); + } + } +); + +containerTestWithIsolatedRedisNoClickhouse( + "simulateInject honors the endpoint filter: a non-matching event is recorded FILTERED, not routed", + async ({ prisma, redisOptions }) => { + const endpoint = await createEndpoint(prisma, { + filter: "event.type == 'payment_intent.succeeded'", + }); + const { triggerTask, calls } = makeTriggerTaskStub(); + const engine = buildEngine(prisma, redisOptions, triggerTask); + + try { + const body = JSON.stringify({ id: "evt_simulate_filtered", type: "charge.refunded" }); + const simulated = await engine.simulateInject({ + opaqueId: endpoint.opaqueId, + rawBytes: new TextEncoder().encode(body), + headers: {}, + url: `https://api.example.com/webhooks/v1/ingest/${endpoint.opaqueId}`, + }); + expect(simulated.outcome).toBe("accepted"); + if (simulated.outcome !== "accepted") return; + + const delivery = await prisma.webhookDelivery.findFirst({ + where: { id: simulated.deliveryId }, + }); + expect(delivery?.status).toBe("FILTERED"); + expect(calls).toHaveLength(0); + } finally { + await engine.quit(); + } + } +); diff --git a/internal-packages/webhook-engine/src/engine/partitions.test.ts b/internal-packages/webhook-engine/src/engine/partitions.test.ts new file mode 100644 index 00000000000..d3f61f9bc92 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/partitions.test.ts @@ -0,0 +1,116 @@ +import { postgresTest } from "@internal/testcontainers"; +import { PrismaClient } from "@trigger.dev/database"; +import { expect } from "vitest"; +import { + addDays, + createPartition, + dayBucket, + detachPartitionConcurrently, + dropPartition, + ensurePartitions, + floorDayUTC, + listDatedPartitions, + partitionExists, + partitionName, + recoverInterruptedDetaches, +} from "./partitions.js"; + +// `prisma db push` builds WebhookDelivery as a plain table (partitioning lives only in the migration +// SQL), so recreate it as the partitioned parent (no DEFAULT, matching the migration) before +// exercising the in-app partition manager. +async function makePartitioned(prisma: PrismaClient) { + await prisma.$executeRawUnsafe(`DROP TABLE IF EXISTS "WebhookDelivery" CASCADE`); + await prisma.$executeRawUnsafe(` + CREATE TABLE "WebhookDelivery" ( + "id" TEXT NOT NULL, + "friendlyId" TEXT NOT NULL, + "webhookEndpointId" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "runtimeEnvironmentId" TEXT NOT NULL, + "environmentType" "RuntimeEnvironmentType" NOT NULL, + "externalDeliveryId" TEXT NOT NULL, + "idempotencyKey" TEXT NOT NULL, + "runId" TEXT, + "status" "WebhookDeliveryStatus" NOT NULL DEFAULT 'PENDING', + "parsedEvent" JSONB, + "headers" JSONB, + "rawBodyHash" TEXT, + "errorMessage" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "processedAt" TIMESTAMP(3), + CONSTRAINT "WebhookDelivery_pkey" PRIMARY KEY ("id","createdAt") + ) PARTITION BY RANGE ("createdAt")`); + await prisma.$executeRawUnsafe( + `CREATE INDEX "WebhookDelivery_webhookEndpointId_createdAt_idx" ON "WebhookDelivery"("webhookEndpointId","createdAt" DESC)` + ); +} + +postgresTest("ensurePartitions creates the window and is idempotent", async ({ prisma }) => { + await makePartitioned(prisma); + const now = new Date(Date.UTC(2026, 6, 15)); + + const first = await ensurePartitions(prisma, { now, lookaheadDays: 7, retentionDays: 3 }); + // retention(3) back .. lookahead(7) forward, inclusive of both ends = 11 day buckets. + expect(first.created).toHaveLength(11); + expect(await partitionExists(prisma, partitionName(floorDayUTC(now)))).toBe(true); + + const second = await ensurePartitions(prisma, { now, lookaheadDays: 7, retentionDays: 3 }); + expect(second.created).toHaveLength(0); + expect(second.existing).toHaveLength(11); +}); + +postgresTest("createPartition is created-then-exists", async ({ prisma }) => { + await makePartitioned(prisma); + const bucket = dayBucket(new Date(Date.UTC(2026, 7, 1))); + + expect(await createPartition(prisma, bucket)).toBe("created"); + expect(await createPartition(prisma, bucket)).toBe("exists"); + expect(await partitionExists(prisma, bucket.name)).toBe(true); +}); + +postgresTest("detachPartitionConcurrently then drop removes a partition", async ({ prisma }) => { + await makePartitioned(prisma); + const bucket = dayBucket(new Date(Date.UTC(2026, 0, 1))); + await createPartition(prisma, bucket); + expect(await partitionExists(prisma, bucket.name)).toBe(true); + + await detachPartitionConcurrently(prisma, bucket.name); + await dropPartition(prisma, bucket.name); + expect(await partitionExists(prisma, bucket.name)).toBe(false); +}); + +postgresTest("ensurePartitions drops children past the retention window", async ({ prisma }) => { + await makePartitioned(prisma); + const now = new Date(Date.UTC(2026, 6, 15)); + + const old = dayBucket(addDays(floorDayUTC(now), -30)); + await createPartition(prisma, old); + expect(await partitionExists(prisma, old.name)).toBe(true); + + const result = await ensurePartitions(prisma, { now, lookaheadDays: 7, retentionDays: 3 }); + expect(result.dropped).toContain(old.name); + expect(result.deferred).toHaveLength(0); + expect(await partitionExists(prisma, old.name)).toBe(false); + expect((await listDatedPartitions(prisma)).some((p) => p.name === old.name)).toBe(false); +}); + +postgresTest( + "recoverInterruptedDetaches drops a detached-but-not-dropped leftover", + async ({ prisma }) => { + await makePartitioned(prisma); + // A crash between DETACH and DROP leaves a standalone dated table that's no longer a partition. + await prisma.$executeRawUnsafe( + `CREATE TABLE "WebhookDelivery_2020_01_01" (LIKE "WebhookDelivery")` + ); + expect(await partitionExists(prisma, "WebhookDelivery_2020_01_01")).toBe(true); + // Not a partition, so the manager doesn't list it among dated partitions. + expect( + (await listDatedPartitions(prisma)).some((p) => p.name === "WebhookDelivery_2020_01_01") + ).toBe(false); + + await recoverInterruptedDetaches(prisma); + expect(await partitionExists(prisma, "WebhookDelivery_2020_01_01")).toBe(false); + } +); diff --git a/internal-packages/webhook-engine/src/engine/partitions.ts b/internal-packages/webhook-engine/src/engine/partitions.ts new file mode 100644 index 00000000000..399f6cce8d5 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/partitions.ts @@ -0,0 +1,245 @@ +import type { WebhookDatabase } from "@trigger.dev/database"; + +// Two identifier forms for the PascalCase Prisma table name. DDL must DOUBLE-QUOTE +// (Postgres folds unquoted identifiers to lowercase); pg_class.relname stores the +// bare case-preserved name, so catalog lookups bind the bare form. +export const PARENT_DDL = `"WebhookDelivery"`; +export const PARENT_NAME = `WebhookDelivery`; + +// --------------------------------------------------------------------------- +// Day-bucket math (everything in UTC, matching how the migration writes bounds) +// --------------------------------------------------------------------------- + +export type Bucket = { lo: Date; hi: Date; name: string }; // name bare, quoted at SQL build + +export function floorDayUTC(d: Date): Date { + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); +} + +export function addDays(d: Date, n: number): Date { + return new Date(d.getTime() + n * 86_400_000); +} + +export function partitionName(lo: Date): string { + const y = lo.getUTCFullYear(); + const m = String(lo.getUTCMonth() + 1).padStart(2, "0"); + const day = String(lo.getUTCDate()).padStart(2, "0"); + return `WebhookDelivery_${y}_${m}_${day}`; // bare Prisma table name +} + +export function dayBucket(lo: Date): Bucket { + const floored = floorDayUTC(lo); + const hi = addDays(floored, 1); + return { lo: floored, hi, name: partitionName(floored) }; +} + +/** All day buckets covering [start, end] inclusive of the day containing end. */ +export function dayBuckets(start: Date, end: Date): Bucket[] { + const out: Bucket[] = []; + let cur = floorDayUTC(start); + const last = floorDayUTC(end); + while (cur.getTime() <= last.getTime()) { + out.push(dayBucket(cur)); + cur = addDays(cur, 1); + } + return out; +} + +// Identifiers are built only from our own YYYY_MM_DD naming, never user input, so +// inlining them (and the bound literals, which Postgres requires as constants in DDL) +// is safe. We still assert the shape as a belt-and-braces guard. Caller quotes: `"${safeName(b.name)}"`. +function safeName(name: string): string { + if (!/^WebhookDelivery_\d{4}_\d{2}_\d{2}$/.test(name)) { + throw new Error(`refusing to use unexpected partition name: ${name}`); + } + return name; +} + +// --------------------------------------------------------------------------- +// Create / detach / drop primitives +// --------------------------------------------------------------------------- + +export async function partitionExists(prisma: WebhookDatabase, name: string): Promise { + // to_regclass takes a quoted identifier string so it preserves the PascalCase. Cast to text: + // Prisma's query engine can't deserialize the raw regclass OID type. + const r = await prisma.$queryRawUnsafe<{ oid: string | null }[]>( + `SELECT to_regclass($1)::text AS oid`, + `"${safeName(name)}"` + ); + return r[0]?.oid != null; +} + +/** + * Create one dated child as a true PARTITION OF the parent (inherits the parent indexes, no + * validating scan). There is no DEFAULT partition, so the lookahead window MUST stay ahead of + * ingest: an insert whose createdAt has no matching child errors instead of landing in a default. + */ +export async function createPartition( + prisma: WebhookDatabase, + b: Bucket +): Promise<"created" | "exists"> { + if (await partitionExists(prisma, b.name)) return "exists"; + const name = `"${safeName(b.name)}"`; + await prisma.$executeRawUnsafe( + `CREATE TABLE ${name} PARTITION OF ${PARENT_DDL} ` + + `FOR VALUES FROM ('${b.lo.toISOString()}') TO ('${b.hi.toISOString()}')` + ); + return "created"; +} + +/** + * Detach a child without blocking ingest: CONCURRENTLY takes only SHARE UPDATE EXCLUSIVE on the + * parent (no conflict with the ROW EXCLUSIVE that inserts hold) and waits just for in-flight txns + * to finish. It MUST run outside a transaction block, so it goes out as a standalone statement. + */ +export async function detachPartitionConcurrently( + prisma: WebhookDatabase, + name: string +): Promise { + await prisma.$executeRawUnsafe( + `ALTER TABLE ${PARENT_DDL} DETACH PARTITION "${safeName(name)}" CONCURRENTLY` + ); +} + +/** Drop a (now standalone, post-detach) table. With no partition attachment it takes no parent lock. */ +export async function dropPartition(prisma: WebhookDatabase, name: string): Promise { + await prisma.$executeRawUnsafe(`DROP TABLE IF EXISTS "${safeName(name)}"`); +} + +/** + * Clean up a detach a crash left half-done, so the next retention pass can proceed: + * (a) a child stuck "pending detach" (interrupted mid-CONCURRENTLY) is FINALIZEd then dropped — + * Postgres allows only one pending detach at a time, so this must run before any new detach; + * (b) a child fully detached but not yet dropped is a standalone leftover table to drop. + */ +export async function recoverInterruptedDetaches(prisma: WebhookDatabase): Promise { + const pending = await prisma.$queryRawUnsafe<{ name: string }[]>( + `SELECT c.relname::text AS name FROM pg_inherits i + JOIN pg_class c ON c.oid = i.inhrelid + JOIN pg_class p ON p.oid = i.inhparent + WHERE p.relname = $1 AND i.inhdetachpending`, + PARENT_NAME + ); + for (const { name } of pending) { + await prisma.$executeRawUnsafe( + `ALTER TABLE ${PARENT_DDL} DETACH PARTITION "${safeName(name)}" FINALIZE` + ); + await dropPartition(prisma, name); + } + + const leftovers = await prisma.$queryRawUnsafe<{ name: string }[]>( + `SELECT c.relname::text AS name FROM pg_class c + WHERE c.relkind = 'r' + AND c.relname ~ '^WebhookDelivery_[0-9]{4}_[0-9]{2}_[0-9]{2}$' + AND NOT EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = c.oid)` + ); + for (const { name } of leftovers) { + await dropPartition(prisma, name); + } +} + +// --------------------------------------------------------------------------- +// The in-app manager (what the @internal/webhook-engine ensurePartitions cron runs) +// --------------------------------------------------------------------------- + +export type EnsureOptions = { + now: Date; + lookaheadDays: number; // pre-create this many days ahead (7 to 14); MUST stay ahead of ingest + retentionDays: number; // keep this many days back; drop colder children +}; + +export type EnsureResult = { + created: string[]; + dropped: string[]; + existing: string[]; + deferred: string[]; // children we couldn't detach/drop this run; retried next run +}; + +export async function ensurePartitions( + prisma: WebhookDatabase, + opts: EnsureOptions +): Promise { + const today = floorDayUTC(opts.now); + const start = addDays(today, -opts.retentionDays); + const end = addDays(today, opts.lookaheadDays); + + const result: EnsureResult = { created: [], dropped: [], existing: [], deferred: [] }; + + for (const b of dayBuckets(start, end)) { + const outcome = await createPartition(prisma, b); + if (outcome === "created") result.created.push(b.name); + else result.existing.push(b.name); + } + + // Finish any detach a prior run left half-done, then concurrently detach + drop each dated child + // whose whole range is older than the retention window. CONCURRENTLY keeps ingest unblocked; a + // child we can't process now (e.g. a transient error) is left for the next run. + await recoverInterruptedDetaches(prisma); + for (const child of await listDatedPartitions(prisma)) { + if (child.hi.getTime() <= start.getTime()) { + try { + await detachPartitionConcurrently(prisma, child.name); + await dropPartition(prisma, child.name); + result.dropped.push(child.name); + } catch { + result.deferred.push(child.name); + } + } + } + + return result; +} + +// --------------------------------------------------------------------------- +// Introspection +// --------------------------------------------------------------------------- + +export type PartitionInfo = { + name: string; + bound: string; + approxRows: number; + bytes: number; + lo?: Date; + hi?: Date; +}; + +export async function listPartitions(prisma: WebhookDatabase): Promise { + const rows = await prisma.$queryRawUnsafe< + { name: string; bound: string; approx_rows: bigint; bytes: bigint }[] + >( + `SELECT c.relname::text AS name, + pg_get_expr(c.relpartbound, c.oid) AS bound, + c.reltuples::bigint AS approx_rows, + pg_total_relation_size(c.oid) AS bytes + FROM pg_inherits i + JOIN pg_class c ON c.oid = i.inhrelid + JOIN pg_class p ON p.oid = i.inhparent + WHERE p.relname = $1 + ORDER BY c.relname`, + PARENT_NAME + ); + return rows.map((row) => { + let lo: Date | undefined; + let hi: Date | undefined; + const m = row.name.match(/^WebhookDelivery_(\d{4})_(\d{2})_(\d{2})$/); + if (m) { + lo = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))); + hi = addDays(lo, 1); + } + return { + name: row.name, + bound: row.bound, + approxRows: Number(row.approx_rows), + bytes: Number(row.bytes), + lo, + hi, + }; + }); +} + +export async function listDatedPartitions( + prisma: WebhookDatabase +): Promise>[]> { + const all = await listPartitions(prisma); + return all.filter((p) => p.lo && p.hi).map((p) => ({ name: p.name, lo: p.lo!, hi: p.hi! })); +} diff --git a/internal-packages/webhook-engine/src/engine/sessionKey.test.ts b/internal-packages/webhook-engine/src/engine/sessionKey.test.ts new file mode 100644 index 00000000000..7f3bd3d5bc8 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/sessionKey.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { evaluateSessionKeyTemplate } from "./sessionKey.js"; + +const meta = { + externalRef: "inst_42", + tenantId: "tnt_1", + id: "github-issues", + source: "github", + deliveryId: "dlv_abc", +}; + +const ns = (body: unknown, header: Record = {}) => ({ + body, + webhook: meta, + header, +}); + +describe("evaluateSessionKeyTemplate", () => { + it("resolves a nested body path", () => { + expect(evaluateSessionKeyTemplate("{body.issue.id}", ns({ issue: { id: 99 } }))).toBe("99"); + }); + + it("treats a bare path as the body namespace", () => { + expect(evaluateSessionKeyTemplate("{issue.id}", ns({ issue: { id: 7 } }))).toBe("7"); + }); + + it("resolves webhook meta and composes literals", () => { + expect( + evaluateSessionKeyTemplate("{webhook.externalRef}-{body.issue.id}", ns({ issue: { id: 5 } })) + ).toBe("inst_42-5"); + }); + + it("matches headers case-insensitively", () => { + expect( + evaluateSessionKeyTemplate("{header.x-installation}", ns({}, { "X-Installation": "abc" })) + ).toBe("abc"); + }); + + it("resolves a composite across all three namespaces", () => { + expect( + evaluateSessionKeyTemplate( + "{webhook.tenantId}/{header.x-inst}/{body.issue.id}", + ns({ issue: { id: 3 } }, { "x-inst": "i9" }) + ) + ).toBe("tnt_1/i9/3"); + }); + + it("returns undefined when a body path is missing", () => { + expect( + evaluateSessionKeyTemplate("{body.issue.nope}", ns({ issue: { id: 1 } })) + ).toBeUndefined(); + }); + + it("returns undefined for an unknown webhook meta field", () => { + expect(evaluateSessionKeyTemplate("{webhook.installationId}", ns({}))).toBeUndefined(); + }); + + it("returns undefined for a missing header", () => { + expect( + evaluateSessionKeyTemplate("{header.x-nope}", ns({}, { "x-inst": "i9" })) + ).toBeUndefined(); + }); + + it("returns undefined when a value is empty string", () => { + expect(evaluateSessionKeyTemplate("{body.region}", ns({ region: "" }))).toBeUndefined(); + }); + + it("coerces numbers and booleans (including 0/false) to strings", () => { + expect(evaluateSessionKeyTemplate("{body.n}-{body.b}", ns({ n: 0, b: false }))).toBe("0-false"); + }); + + it("falls back to the next path with || when the first is empty (Slack thread_ts || ts)", () => { + const template = "{body.channel}:{body.thread_ts || body.ts}"; + // reply: thread_ts present -> used + expect( + evaluateSessionKeyTemplate(template, ns({ channel: "C1", thread_ts: "t1", ts: "m2" })) + ).toBe("C1:t1"); + // thread start: no thread_ts -> falls back to ts + expect(evaluateSessionKeyTemplate(template, ns({ channel: "C1", ts: "m2" }))).toBe("C1:m2"); + }); + + it("uses the first non-empty across a || chain, else undefined", () => { + expect(evaluateSessionKeyTemplate("{body.a || body.b || body.c}", ns({ c: "z" }))).toBe("z"); + expect(evaluateSessionKeyTemplate("{body.a || body.b}", ns({}))).toBeUndefined(); + }); +}); diff --git a/internal-packages/webhook-engine/src/engine/sessionKey.ts b/internal-packages/webhook-engine/src/engine/sessionKey.ts new file mode 100644 index 00000000000..b1803deed8a --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/sessionKey.ts @@ -0,0 +1,54 @@ +// Runtime evaluation of a session routing key template. The template ({body.x}/{webhook.x}/{header.x}) +// is compiled from the SDK's session.webhook `key` at deploy-sync; here we resolve it against a live +// delivery to produce the session externalId. Returns undefined if any placeholder is missing/empty, +// so the engine can fail the delivery rather than route it to a garbage session key. + +export type SessionKeyNamespaces = { + body: unknown; // the parsed event body + webhook: Record; // endpoint meta: externalRef, tenantId, id, source, deliveryId + header: Record; // inbound request headers (matched case-insensitively) +}; + +export function evaluateSessionKeyTemplate( + template: string, + ns: SessionKeyNamespaces +): string | undefined { + let ok = true; + const out = template.replace(/\{([^}]+)\}/g, (_match, raw: string) => { + // A placeholder may list fallbacks: `{a || b || c}` resolves to the first non-empty path (so a + // Slack thread-start with no thread_ts falls back to ts). Missing/empty everywhere fails the key. + for (const path of raw.split("||")) { + const value = resolveKeyPath(path.trim(), ns); + if (value !== undefined && value !== null && value !== "") return String(value); + } + ok = false; + return ""; + }); + return ok ? out : undefined; +} + +function resolveKeyPath(path: string, ns: SessionKeyNamespaces): unknown { + if (path.startsWith("webhook.")) { + return ns.webhook[path.slice("webhook.".length)]; + } + if (path.startsWith("header.")) { + const name = path.slice("header.".length).toLowerCase(); + for (const [key, value] of Object.entries(ns.header)) { + if (key.toLowerCase() === name) return value; + } + return undefined; + } + // body. prefix or a bare path both resolve against the event body. + const dotted = path.startsWith("body.") ? path.slice("body.".length) : path; + return walkPath(ns.body, dotted); +} + +// Resolve a dotted path over an arbitrary object (shared with the ingest handshake hook). +export function walkPath(root: unknown, dotted: string): unknown { + let current: unknown = root; + for (const segment of dotted.split(".")) { + if (current === null || typeof current !== "object") return undefined; + current = (current as Record)[segment]; + } + return current; +} diff --git a/internal-packages/webhook-engine/src/engine/signing/index.ts b/internal-packages/webhook-engine/src/engine/signing/index.ts new file mode 100644 index 00000000000..82e56bf03e1 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/signing/index.ts @@ -0,0 +1,193 @@ +import type { + WebhookHmacConfig, + WebhookSharedSecretConfig, + WebhookUrlSecretConfig, + WebhookValueSource, + WebhookVerifierConfig, +} from "@trigger.dev/core/v3"; +import { randomUUID } from "node:crypto"; +import { readPath, tryParseJson } from "../verification/derive.js"; +import { buildSigningBytes, deriveHmacKey, hmacDigest } from "../verification/util.js"; + +export type SignResult = + | { ok: true; headers: Record; url: string; body: Uint8Array } + | { ok: false; notSignable: boolean; error: string }; + +export type SignArgs = { + config: WebhookVerifierConfig; + secret: string; + rawBody: Uint8Array; + url: string; + headers?: Record; + nowMs?: number; +}; + +/** + * Produce a request the engine verifier accepts under the same config + secret. The inverse of + * `verification/`: it owns both the signed value and its placement, so re-parsing the request it + * builds yields byte-identical signing bytes and a matching signature. Asymmetric and url-secret + * `path` placement return `{ ok: false, notSignable: true }` (simulate-only downstream). + */ +export function signWithVerifierConfig(args: SignArgs): SignResult { + const { config } = args; + switch (config.scheme) { + case "hmac": + return signHmac(config, args); + case "shared-secret": + return signSharedSecret(config, args); + case "url-secret": + return signUrlSecret(config, args); + case "asymmetric": + return { + ok: false, + notSignable: true, + error: + "asymmetric signatures cannot be produced (only the public key is held); use unsigned/tampered or simulate mode", + }; + } +} + +function lowerCaseHeaders(headers: Record): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v; + return out; +} + +function readStringSource( + source: WebhookValueSource, + args: SignArgs, + headersLc: Record +): string | undefined { + switch (source.from) { + case "header": + return headersLc[source.name.toLowerCase()]; + case "url": + return args.url; + case "constant": + return source.value; + case "body": { + const parsed = tryParseJson(args.rawBody).parsedEvent as Record | undefined; + const v = readPath(parsed, source.path); + return typeof v === "string" || typeof v === "number" ? String(v) : undefined; + } + case "signatureField": + return undefined; + } +} + +function signHmac(cfg: WebhookHmacConfig, args: SignArgs): SignResult { + const headers: Record = { ...(args.headers ?? {}) }; + const headersLc = lowerCaseHeaders(headers); + const now = args.nowMs ?? Date.now(); + + let timestampValue = ""; + if (cfg.timestamp) { + const src = cfg.timestamp.source; + if (src.from === "header" || src.from === "signatureField") { + timestampValue = + cfg.timestamp.unit === "milliseconds" ? String(now) : String(Math.floor(now / 1000)); + if (src.from === "header") { + headers[src.name] = timestampValue; + headersLc[src.name.toLowerCase()] = timestampValue; + } + } else { + timestampValue = readStringSource(src, args, headersLc) ?? ""; + } + } + + let signingBytes: Uint8Array; + if (cfg.signingString === "raw") { + signingBytes = args.rawBody; + } else { + const vars: Record = { timestamp: timestampValue }; + for (const [name, source] of Object.entries(cfg.signingString.vars ?? {})) { + let v: string | undefined; + if (source.from === "header") { + v = headersLc[source.name.toLowerCase()] ?? randomUUID(); + headers[source.name] = v; + headersLc[source.name.toLowerCase()] = v; + } else { + v = readStringSource(source, args, headersLc); + } + vars[name] = v ?? ""; + } + signingBytes = buildSigningBytes(cfg.signingString.template, args.rawBody, vars); + } + + const digest = hmacDigest( + cfg.algorithm, + deriveHmacKey(args.secret, cfg.secret), + signingBytes, + cfg.encoding + ); + headers[cfg.signatureHeader] = buildSignatureHeader(cfg, digest, timestampValue); + return { ok: true, headers, url: args.url, body: args.rawBody }; +} + +/** Inverse of `parseSignatureHeader`: assemble the element(s) the verifier will split back out. */ +function buildSignatureHeader( + cfg: WebhookHmacConfig, + digest: string, + timestampValue: string +): string { + const ex = cfg.signature; + const elements: string[] = []; + if (cfg.timestamp?.source.from === "signatureField" && ex?.fieldSeparator) { + elements.push(`${cfg.timestamp.source.field}${ex.fieldSeparator}${timestampValue}`); + } + if (ex?.field && ex.fieldSeparator) { + elements.push(`${ex.field}${ex.fieldSeparator}${digest}`); + } else { + elements.push(digest); + } + return elements.join(ex?.itemSeparator ?? ""); +} + +function signSharedSecret(cfg: WebhookSharedSecretConfig, args: SignArgs): SignResult { + const headers: Record = { ...(args.headers ?? {}) }; + let body = args.rawBody; + switch (cfg.placement) { + case "header": + headers[cfg.fieldName ?? ""] = args.secret; + break; + case "bearer": + headers["authorization"] = `Bearer ${args.secret}`; + break; + case "basic": + headers["authorization"] = `Basic ${Buffer.from(`:${args.secret}`).toString("base64")}`; + break; + case "body": { + const parsed = tryParseJson(args.rawBody).parsedEvent; + if (parsed == null || typeof parsed !== "object") { + return { + ok: false, + notSignable: false, + error: "body placement requires a JSON object body", + }; + } + const next = { ...(parsed as Record), [cfg.fieldName ?? ""]: args.secret }; + body = new TextEncoder().encode(JSON.stringify(next)); + break; + } + } + return { ok: true, headers, url: args.url, body }; +} + +function signUrlSecret(cfg: WebhookUrlSecretConfig, args: SignArgs): SignResult { + if (cfg.placement === "path") { + return { + ok: false, + notSignable: true, + error: + "url-secret path placement cannot be represented on the fixed ingress URL; use simulate mode", + }; + } + const url = new URL(args.url); + url.searchParams.set(cfg.paramName, args.secret); + return { + ok: true, + headers: { ...(args.headers ?? {}) }, + url: url.toString(), + body: args.rawBody, + }; +} diff --git a/internal-packages/webhook-engine/src/engine/signing/signing.test.ts b/internal-packages/webhook-engine/src/engine/signing/signing.test.ts new file mode 100644 index 00000000000..2c7cac720b7 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/signing/signing.test.ts @@ -0,0 +1,169 @@ +import type { + WebhookAsymmetricConfig, + WebhookHmacConfig, + WebhookSharedSecretConfig, + WebhookUrlSecretConfig, + WebhookVerifierConfig, +} from "@trigger.dev/core/v3"; +import { describe, expect, it } from "vitest"; +import { verify } from "../verification/index.js"; +import { signWithVerifierConfig } from "./index.js"; + +const INGRESS_URL = "https://example.com/webhooks/v1/ingest/opaque123"; +const NOW = 1783000000000; +const BODY = new TextEncoder().encode( + JSON.stringify({ id: "evt_123", event_id: "sqevt_123", type: "test.event" }) +); + +function roundTrip(config: WebhookVerifierConfig, secret: string) { + const signed = signWithVerifierConfig({ + config, + secret, + rawBody: BODY, + url: INGRESS_URL, + nowMs: NOW, + }); + if (!signed.ok) throw new Error(`expected signable: ${signed.error}`); + return verify( + { kind: "config", config }, + { rawBytes: signed.body, headers: signed.headers, url: signed.url, secret, nowMs: NOW } + ); +} + +const stripe: WebhookHmacConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { source: { from: "signatureField", field: "t" }, toleranceSeconds: 300 }, + signingString: { template: "{timestamp}.{body}" }, + idempotencyField: { from: "body", name: "id" }, +}; + +const github: WebhookHmacConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-hub-signature-256", + signature: { fieldSeparator: "=", field: "sha256" }, + signingString: "raw", + idempotencyField: { from: "header", name: "x-github-delivery" }, +}; + +const svix: WebhookHmacConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "base64", + signatureHeader: "svix-signature", + signature: { itemSeparator: " ", fieldSeparator: ",", field: "v1" }, + timestamp: { source: { from: "header", name: "svix-timestamp" }, toleranceSeconds: 300 }, + signingString: { + template: "{id}.{timestamp}.{body}", + vars: { id: { from: "header", name: "svix-id" } }, + }, + secret: { encoding: "base64", stripPrefix: "whsec_" }, + idempotencyField: { from: "header", name: "svix-id" }, +}; + +const square: WebhookHmacConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "base64", + signatureHeader: "x-square-hmacsha256-signature", + signature: {}, + signingString: { template: "{url}{body}", vars: { url: { from: "url" } } }, + idempotencyField: { from: "body", name: "event_id" }, +}; + +const discord: WebhookAsymmetricConfig = { + scheme: "asymmetric", + algorithm: "ed25519", + encoding: "hex", + signatureHeader: "x-signature-ed25519", + signature: {}, + timestamp: { source: { from: "header", name: "x-signature-timestamp" } }, + signingString: { template: "{timestamp}{body}" }, + publicKeyEncoding: "raw-hex", +}; + +const svixSecret = `whsec_${Buffer.from("svix-signing-key").toString("base64")}`; + +describe("signWithVerifierConfig round-trips through the verifier", () => { + it("custom hmac (raw body, bare signature) — the demo-webhook shape", () => { + const custom: WebhookHmacConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-webhook-signature", + signature: {}, + signingString: "raw", + }; + expect(roundTrip(custom, "shhh-custom-secret").ok).toBe(true); + }); + + it("stripe (hex, t=..,v1=.. with signatureField timestamp)", () => { + expect(roundTrip(stripe, "whsec_stripe_test").ok).toBe(true); + }); + + it("github (prefixed sha256=, raw body)", () => { + expect(roundTrip(github, "github_integrator_secret").ok).toBe(true); + }); + + it("svix (base64 v1,, header timestamp + id var, whsec base64 secret)", () => { + expect(roundTrip(svix, svixSecret).ok).toBe(true); + }); + + it("square (bare base64 over {url}{body})", () => { + expect(roundTrip(square, "square_signature_key").ok).toBe(true); + }); + + it("shared-secret header / bearer / basic / body all verify", () => { + const placements: WebhookSharedSecretConfig["placement"][] = [ + "header", + "bearer", + "basic", + "body", + ]; + for (const placement of placements) { + const config: WebhookSharedSecretConfig = { + scheme: "shared-secret", + placement, + fieldName: + placement === "header" ? "x-api-key" : placement === "body" ? "apiKey" : undefined, + }; + expect(roundTrip(config, "the-shared-secret").ok, `placement=${placement}`).toBe(true); + } + }); + + it("url-secret query verifies", () => { + const config: WebhookUrlSecretConfig = { + scheme: "url-secret", + placement: "query", + paramName: "token", + }; + expect(roundTrip(config, "url-secret-value").ok).toBe(true); + }); + + it("url-secret path is not signable (fixed ingress path)", () => { + const config: WebhookUrlSecretConfig = { + scheme: "url-secret", + placement: "path", + paramName: "token", + }; + const r = signWithVerifierConfig({ config, secret: "x", rawBody: BODY, url: INGRESS_URL }); + expect(r.ok).toBe(false); + expect(r.ok === false && r.notSignable).toBe(true); + }); + + it("asymmetric (discord) is not signable (only the public key is held)", () => { + const r = signWithVerifierConfig({ + config: discord, + secret: "deadbeef", + rawBody: BODY, + url: INGRESS_URL, + }); + expect(r.ok).toBe(false); + expect(r.ok === false && r.notSignable).toBe(true); + }); +}); diff --git a/internal-packages/webhook-engine/src/engine/types.ts b/internal-packages/webhook-engine/src/engine/types.ts new file mode 100644 index 00000000000..ed79ccaf764 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/types.ts @@ -0,0 +1,110 @@ +import { Logger } from "@trigger.dev/core/logger"; +import { Meter, Tracer } from "@internal/tracing"; +import { WebhookDatabase } from "@trigger.dev/database"; +import { RedisOptions } from "@internal/redis"; + +export type WebhookDeliverTaskErrorType = "QUEUE_LIMIT" | "SYSTEM_ERROR" | "NOT_FOUND"; + +export type TriggerWebhookTaskParams = { + environmentId: string; + taskId: string; + idempotencyKey: string; // = externalDeliveryId; the Run Engine correctness gate + idempotencyKeyExpiresAt: Date; // provider retry window + payload: unknown; // delivery.parsedEvent, already JSON + headers: Record; // inbound request headers -> onEvent({ headers }) + identityTags: string[]; // endpoint identity -> run tags + endpointMetadata: unknown; // endpoint.metadata -> run metadata +}; + +export interface TriggerWebhookTaskCallback { + (params: TriggerWebhookTaskParams): Promise<{ + success: boolean; + runId?: string; // persisted onto WebhookDelivery.runId on success + error?: string; + errorType?: WebhookDeliverTaskErrorType; + }>; +} + +export interface WebhookEngineOptions { + logger?: Logger; + logLevel?: string; + prisma: WebhookDatabase; + redis: RedisOptions; + worker: { + concurrency: number; + workers?: number; + tasksPerWorker?: number; + pollIntervalMs?: number; + shutdownTimeoutMs?: number; + disabled?: boolean; + }; + partitions?: { + ensureSchedule?: string; + ensureJitterInMs?: number; + lookaheadDays?: number; // 7..14; how many days ahead to pre-create + retentionDays?: number; // keep this many days back; drop colder children + }; + tracer?: Tracer; + meter?: Meter; + frontGate?: { defaultTtlSeconds?: number; maxTtlSeconds?: number; claimTtlSeconds?: number }; + // Hot-path cache for the endpoint + resolved signing secret, keyed by opaqueId. ttlMs <= 0 disables. + endpointCache?: { ttlMs?: number; maxSize?: number }; + triggerTask: TriggerWebhookTaskCallback; + // Q4: injected so the engine never imports the webapp SecretStore. Returns the + // plaintext signing secret, or undefined/empty so ingest fails closed. + resolveSigningSecret: (key: string) => Promise; + // Session routing: find-or-create the session on the resolved key and append the action envelope. + deliverToSession?: DeliverWebhookToSessionCallback; +} + +export type DeliverWebhookToSessionParams = { + environmentId: string; + taskIdentifier: string; // the claiming agent; the session's task + externalId: string; // resolved from the routing target's keyTemplate + deliverAs: "action" | "message"; // "action" -> onAction envelope; "message" -> a channel turn + actionType?: string; // becomes the action envelope's `type` (deliverAs "action") + connectorId?: string; // the channel connector id (deliverAs "message"); the run resolves inbound by it + event: unknown; // delivery.parsedEvent + source: string; // provider tag + headers: Record; + deliveryId: string; // externalDeliveryId; also the S2 part id for idempotent re-append + triggerConfigTemplate?: Record; + idempotencyKey: string; + // Evaluated startOn: true (default) allows creating a new session; false means resume-only, so a + // key with no existing session is ignored rather than started. + isSessionStart: boolean; +}; + +export interface DeliverWebhookToSessionCallback { + (params: DeliverWebhookToSessionParams): Promise<{ + success: boolean; + runId?: string; // the session's current run, persisted onto WebhookDelivery.runId + error?: string; + errorType?: WebhookDeliverTaskErrorType; + skipped?: boolean; // resume-only and no session existed: recorded FILTERED, not routed + skippedReason?: string; + }>; +} + +export type IngestInput = { + opaqueId: string; // Q2: globally unique, so ingest resolves the endpoint (and its env id + type) from it + rawBytes: Uint8Array; + headers: Record; + url: string; +}; + +export type ReplayResult = + | { outcome: "replayed"; deliveryId: string; deliveryFriendlyId: string } // new row + run enqueued + | { outcome: "delivery_not_found" } + | { outcome: "endpoint_not_found" } + | { outcome: "unsupported_target" }; // routing target isn't a task + +export type IngestResult = + | { outcome: "accepted"; deliveryId: string; deliveryFriendlyId: string } + | { outcome: "handshake"; body: string } // provider handshake (Slack url_verification) -> 200 echo + | { outcome: "duplicate"; deliveryId: string } // front-gate hit -> 200 + | { outcome: "endpoint_not_found" } // -> 404 + | { outcome: "endpoint_inactive" } // -> 404 + | { outcome: "secret_missing" } // -> 400 (fail-closed) + | { outcome: "verification_failed"; error: string } // -> 400 + | { outcome: "enqueue_failed"; error: string }; // -> 5xx (provider retries) diff --git a/internal-packages/webhook-engine/src/engine/verification/asymmetric.ts b/internal-packages/webhook-engine/src/engine/verification/asymmetric.ts new file mode 100644 index 00000000000..2bfb45bda09 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/asymmetric.ts @@ -0,0 +1,89 @@ +import type { WebhookAsymmetricConfig as AsymmetricConfig } from "@trigger.dev/core/v3"; +import { createPublicKey, verify as cryptoVerify, type KeyObject } from "node:crypto"; +import { deriveIdempotencyKey, tryParseJson } from "./derive.js"; +import { prepareSignedVerification } from "./parse.js"; +import type { SchemeVerifier, VerifierResult, VerifyInput } from "./types.js"; +import { decodeSignature } from "./util.js"; + +// SPKI DER prefix for an Ed25519 public key (RFC 8410): 12-byte header + 32 raw key bytes. +// Lets us accept a raw Ed25519 key (e.g. Discord's hex public key) without a PEM wrapper. +const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex"); + +function importPublicKey( + stored: string, + encoding: AsymmetricConfig["publicKeyEncoding"], + algorithm: AsymmetricConfig["algorithm"] +): KeyObject { + const enc = encoding ?? "pem"; + if (enc === "pem") return createPublicKey(stored); + if (enc === "spki-der-base64") { + return createPublicKey({ key: Buffer.from(stored, "base64"), format: "der", type: "spki" }); + } + // Raw key bytes. Only Ed25519 has a fixed-size raw key we can wrap into SPKI here. + const raw = Buffer.from(stored, enc === "raw-hex" ? "hex" : "base64"); + if (algorithm === "ed25519") { + return createPublicKey({ + key: Buffer.concat([ED25519_SPKI_PREFIX, raw]), + format: "der", + type: "spki", + }); + } + throw new Error("raw public key encoding is only supported for ed25519"); +} + +export const asymmetricVerifier: SchemeVerifier = { + scheme: "asymmetric", + verify(config, input): VerifierResult { + const cfg = config as AsymmetricConfig; + + const prep = prepareSignedVerification(cfg, input); + if (!prep.ok) return fail(prep.error, cfg, input); + + let key: KeyObject; + try { + key = importPublicKey(input.secret, cfg.publicKeyEncoding, cfg.algorithm); + } catch { + return fail("invalid public key", cfg, input); + } + + // Ed25519 is PureEdDSA (no prehash) → algorithm null; ECDSA/RSA hash with sha256. + const nodeAlgorithm = cfg.algorithm === "ed25519" ? null : "sha256"; + + const matched = prep.signatures.some((sig) => { + try { + return cryptoVerify( + nodeAlgorithm, + prep.signingBytes, + key, + decodeSignature(sig, cfg.encoding) + ); + } catch { + return false; + } + }); + if (!matched) return fail("signature mismatch", cfg, input); + + const idempotencyKey = deriveIdempotencyKey({ + idempotencyField: cfg.idempotencyField, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: prep.timestampValue, + signatureValue: prep.signatureValue, + }); + return { ok: true, idempotencyKey, ...tryParseJson(input.rawBytes) }; + }, +}; + +function fail(error: string, cfg: AsymmetricConfig, input: VerifyInput): VerifierResult { + return { + ok: false, + error, + idempotencyKey: deriveIdempotencyKey({ + idempotencyField: cfg.idempotencyField, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: "", + signatureValue: "", + }), + }; +} diff --git a/internal-packages/webhook-engine/src/engine/verification/derive.ts b/internal-packages/webhook-engine/src/engine/verification/derive.ts new file mode 100644 index 00000000000..88417cecbb8 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/derive.ts @@ -0,0 +1,60 @@ +import { sha256Hex } from "./util.js"; + +export function deriveIdempotencyKey(args: { + idempotencyField?: { from: "header" | "body"; name: string }; // Q9 + headers: Record; + rawBytes: Uint8Array; + timestampValue: string; + signatureValue: string; + formPayloadField?: string; +}): string { + if (args.idempotencyField) { + const { from, name } = args.idempotencyField; + const v = + from === "header" + ? args.headers[name.toLowerCase()] + : readPath( + parseEventBody(args.rawBytes, { formPayloadField: args.formPayloadField }) + .parsedEvent as Record | undefined, + name + ); + if (typeof v === "string" && v.length > 0) return v; + } + const composite = `${sha256Hex(args.rawBytes)}\n${args.timestampValue}\n${args.signatureValue}`; + return sha256Hex(composite); +} + +export function readPath(obj: unknown, path: string): unknown { + return path.split(".").reduce((acc, k) => (acc == null ? acc : acc[k]), obj); +} + +/** + * Parse the verified body into the routed event. Tries JSON first. When that fails and a + * `formPayloadField` is configured, decodes the body as `application/x-www-form-urlencoded` and + * JSON-parses that field's value (Slack interactivity posts `payload=`). The signature was + * already checked over the raw bytes, so this only affects the parsed event, never verification. + */ +export function parseEventBody( + rawBytes: Uint8Array, + opts?: { formPayloadField?: string } +): { parsedEvent?: unknown; error?: string } { + const text = new TextDecoder().decode(rawBytes); + try { + return { parsedEvent: JSON.parse(text) }; + } catch { + void 0; + } + if (opts?.formPayloadField) { + try { + const raw = new URLSearchParams(text).get(opts.formPayloadField); + if (raw != null) return { parsedEvent: JSON.parse(raw) }; + } catch { + void 0; + } + } + return { error: "verified body is not valid JSON" }; +} + +export function tryParseJson(rawBytes: Uint8Array): { parsedEvent?: unknown; error?: string } { + return parseEventBody(rawBytes); +} diff --git a/internal-packages/webhook-engine/src/engine/verification/hmac.ts b/internal-packages/webhook-engine/src/engine/verification/hmac.ts new file mode 100644 index 00000000000..048b78a1662 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/hmac.ts @@ -0,0 +1,54 @@ +import type { WebhookHmacConfig as HmacConfig } from "@trigger.dev/core/v3"; +import type { SchemeVerifier, VerifierResult, VerifyInput } from "./types.js"; +import { hmacDigest, deriveHmacKey, constantTimeEqual } from "./util.js"; +import { deriveIdempotencyKey, parseEventBody } from "./derive.js"; +import { prepareSignedVerification } from "./parse.js"; + +export const hmacVerifier: SchemeVerifier = { + scheme: "hmac", + verify(config, input): VerifierResult { + const cfg = config as HmacConfig; + + // Shared, config-driven parsing: extract signature candidate(s) from the header + // (Stripe `t=,v1=`, GitHub `sha256=`, Svix `v1,` lists, …), validate the timestamp, + // and build the exact signed bytes. No per-provider code beyond this. + const prep = prepareSignedVerification(cfg, input); + if (!prep.ok) return fail(prep.error, cfg, input); + + const key = deriveHmacKey(input.secret, cfg.secret); + const expected = hmacDigest(cfg.algorithm, key, prep.signingBytes, cfg.encoding); + + // Accept if ANY extracted candidate matches (signature rotation / multi-sig lists). + const matched = prep.signatures.some((sig) => constantTimeEqual(sig, expected)); + if (!matched) return fail("signature mismatch", cfg, input); + + const idempotencyKey = deriveIdempotencyKey({ + idempotencyField: cfg.idempotencyField, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: prep.timestampValue, + signatureValue: prep.signatureValue, + formPayloadField: cfg.formPayload?.field, + }); + return { + ok: true, + idempotencyKey, + ...parseEventBody(input.rawBytes, { formPayloadField: cfg.formPayload?.field }), + }; + }, +}; + +function fail(error: string, cfg: HmacConfig, input: VerifyInput): VerifierResult { + return { + ok: false, + error, + idempotencyKey: deriveIdempotencyKey({ + idempotencyField: cfg.idempotencyField, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: "", + signatureValue: "", + formPayloadField: cfg.formPayload?.field, + }), + }; +} diff --git a/internal-packages/webhook-engine/src/engine/verification/index.ts b/internal-packages/webhook-engine/src/engine/verification/index.ts new file mode 100644 index 00000000000..913254bf24e --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/index.ts @@ -0,0 +1,43 @@ +import type { VerifierArtifact, VerifierConfig, VerifierResult, VerifyInput } from "./types.js"; +import { hmacVerifier } from "./hmac.js"; +import { asymmetricVerifier } from "./asymmetric.js"; +import { sharedSecretVerifier } from "./sharedSecret.js"; +import { urlSecretVerifier } from "./urlSecret.js"; +import type { SchemeVerifier } from "./types.js"; + +const SCHEME_REGISTRY: Record = { + hmac: hmacVerifier, + "shared-secret": sharedSecretVerifier, + "url-secret": urlSecretVerifier, + asymmetric: asymmetricVerifier, +}; + +function normalizeHeaders(headers: Record): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(headers)) out[k.toLowerCase()] = v; + return out; +} + +export function verify(artifact: VerifierArtifact, input: VerifyInput): VerifierResult { + const normalized: VerifyInput = { ...input, headers: normalizeHeaders(input.headers) }; + + switch (artifact.kind) { + case "config": + case "preset": { + const config = artifact.config; + const verifier = SCHEME_REGISTRY[config.scheme]; + if (!verifier) { + throw new Error(`No verifier registered for scheme "${config.scheme}"`); + } + return verifier.verify(config, normalized); + } + case "bundle": + throw new Error("Bundle verifiers are not available in this version (P3 seam)"); + default: { + const _exhaustive: never = artifact; + throw new Error(`Unknown verifier artifact kind`); + } + } +} + +export { type VerifyInput, type VerifierResult } from "./types.js"; diff --git a/internal-packages/webhook-engine/src/engine/verification/parse.ts b/internal-packages/webhook-engine/src/engine/verification/parse.ts new file mode 100644 index 00000000000..70eaa2a779c --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/parse.ts @@ -0,0 +1,136 @@ +import type { + WebhookSignatureExtraction, + WebhookSigningString, + WebhookTimestampConfig, + WebhookValueSource, +} from "@trigger.dev/core/v3"; +import { readPath, tryParseJson } from "./derive.js"; +import type { VerifyInput } from "./types.js"; +import { buildSigningBytes } from "./util.js"; + +// The subset of a verifier config shared by every signed scheme (hmac, asymmetric). +export type SignedConfig = { + signatureHeader: string; + signature?: WebhookSignatureExtraction; + timestamp?: WebhookTimestampConfig; + signingString: WebhookSigningString; +}; + +export type PreparedVerification = + | { ok: false; error: string } + | { + ok: true; + signatures: string[]; // candidate signatures; the scheme accepts ANY match + signingBytes: Uint8Array; + timestampValue: string; + signatureValue: string; // first candidate, for the idempotency fallback hash + }; + +// Parse one signature header into (a) candidate signature strings and (b) a field map for +// signatureField lookups (e.g. Stripe `t`). See WebhookSignatureExtraction for the shapes. +export function parseSignatureHeader( + headerValue: string, + extraction?: WebhookSignatureExtraction +): { signatures: string[]; fields: Map } { + const ex = extraction ?? {}; + const rawItems = ex.itemSeparator ? headerValue.split(ex.itemSeparator) : [headerValue]; + const items = ex.trim ? rawItems.map((s) => s.trim()) : rawItems; + + const fields = new Map(); + const signatures: string[] = []; + + for (const item of items) { + if (ex.fieldSeparator) { + const idx = item.indexOf(ex.fieldSeparator); + if (idx === -1) continue; // malformed element, skip + const name = item.slice(0, idx); + const value = item.slice(idx + ex.fieldSeparator.length); + const arr = fields.get(name) ?? []; + arr.push(value); + fields.set(name, arr); + if (ex.field && name === ex.field) signatures.push(value); + } else { + // No field separator → the whole element IS the signature (bare value). + signatures.push(item); + } + } + + return { signatures, fields }; +} + +type ResolveContext = { + headers: Record; + fields: Map; + rawBytes: Uint8Array; + url: string; +}; + +function resolveValueSource(source: WebhookValueSource, ctx: ResolveContext): string | undefined { + switch (source.from) { + case "header": + return ctx.headers[source.name.toLowerCase()]; + case "signatureField": + return ctx.fields.get(source.field)?.[0]; + case "body": { + const parsed = tryParseJson(ctx.rawBytes).parsedEvent as Record | undefined; + const v = readPath(parsed, source.path); + return typeof v === "string" || typeof v === "number" ? String(v) : undefined; + } + case "url": + return ctx.url; + case "constant": + return source.value; + } +} + +// Shared pre-verification: pull the signature(s) out of the header, validate the timestamp +// (replay window), and build the exact bytes to be signed. The scheme-specific code then only +// computes/compares the signature. No per-provider branching lives here — it's all config. +export function prepareSignedVerification( + config: SignedConfig, + input: VerifyInput +): PreparedVerification { + const rawHeader = input.headers[config.signatureHeader.toLowerCase()]; + if (!rawHeader) return { ok: false, error: "missing signature header" }; + + const { signatures, fields } = parseSignatureHeader(rawHeader, config.signature); + if (signatures.length === 0) return { ok: false, error: "no signature found in header" }; + + const ctx: ResolveContext = { + headers: input.headers, + fields, + rawBytes: input.rawBytes, + url: input.url, + }; + + let timestampValue = ""; + if (config.timestamp) { + const v = resolveValueSource(config.timestamp.source, ctx); + if (!v) return { ok: false, error: "missing timestamp" }; + const n = Number(v); + if (Number.isNaN(n)) return { ok: false, error: "unparseable timestamp" }; + timestampValue = v; + if (config.timestamp.toleranceSeconds !== undefined) { + const tsSeconds = config.timestamp.unit === "milliseconds" ? n / 1000 : n; + const nowS = (input.nowMs ?? Date.now()) / 1000; + if (Math.abs(nowS - tsSeconds) > config.timestamp.toleranceSeconds) { + return { ok: false, error: "timestamp outside tolerance" }; + } + } + } + + let signingBytes: Uint8Array; + if (config.signingString === "raw") { + signingBytes = input.rawBytes; + } else { + const vars: Record = { timestamp: timestampValue }; + if (config.signingString.vars) { + for (const [name, source] of Object.entries(config.signingString.vars)) { + vars[name] = resolveValueSource(source, ctx) ?? ""; + } + } + signingBytes = buildSigningBytes(config.signingString.template, input.rawBytes, vars); + } + + return { ok: true, signatures, signingBytes, timestampValue, signatureValue: signatures[0] }; +} diff --git a/internal-packages/webhook-engine/src/engine/verification/sharedSecret.ts b/internal-packages/webhook-engine/src/engine/verification/sharedSecret.ts new file mode 100644 index 00000000000..01618d5a1ab --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/sharedSecret.ts @@ -0,0 +1,64 @@ +import type { WebhookSharedSecretConfig as SharedSecretConfig } from "@trigger.dev/core/v3"; +import type { SchemeVerifier, VerifierResult, VerifyInput } from "./types.js"; +import { constantTimeEqual } from "./util.js"; +import { deriveIdempotencyKey, tryParseJson } from "./derive.js"; + +export const sharedSecretVerifier: SchemeVerifier = { + scheme: "shared-secret", + verify(config, input): VerifierResult { + const cfg = config as Extract; + const provided = extractCandidate(cfg, input); + if (provided === undefined) return failSS("missing shared secret", cfg, input); + if (!constantTimeEqual(provided, input.secret)) + return failSS("shared secret mismatch", cfg, input); + const idempotencyKey = deriveIdempotencyKey({ + idempotencyField: cfg.idempotencyField, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: "", + signatureValue: provided, + }); + return { ok: true, idempotencyKey, ...tryParseJson(input.rawBytes) }; + }, +}; + +function extractCandidate(cfg: any, input: VerifyInput): string | undefined { + switch (cfg.placement) { + case "header": + return input.headers[(cfg.fieldName ?? "").toLowerCase()] || undefined; + case "bearer": { + const h = input.headers["authorization"] ?? ""; + return h.startsWith("Bearer ") ? h.slice(7) : undefined; + } + case "basic": { + const h = input.headers["authorization"] ?? ""; + if (!h.startsWith("Basic ")) return undefined; + const decoded = Buffer.from(h.slice(6), "base64").toString("utf8"); + const idx = decoded.indexOf(":"); + return idx === -1 ? decoded : decoded.slice(idx + 1); // password segment + } + case "body": { + const parsed = tryParseJson(input.rawBytes).parsedEvent as + | Record + | undefined; + const v = parsed?.[cfg.fieldName ?? ""]; + return typeof v === "string" ? v : undefined; + } + default: + return undefined; + } +} + +function failSS(error: string, cfg: any, input: VerifyInput): VerifierResult { + return { + ok: false, + error, + idempotencyKey: deriveIdempotencyKey({ + idempotencyField: cfg.idempotencyField, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: "", + signatureValue: "", + }), + }; +} diff --git a/internal-packages/webhook-engine/src/engine/verification/types.ts b/internal-packages/webhook-engine/src/engine/verification/types.ts new file mode 100644 index 00000000000..0cb553004c4 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/types.ts @@ -0,0 +1,20 @@ +import type { + WebhookVerifierArtifact as VerifierArtifact, + WebhookVerifierConfig as VerifierConfig, + WebhookVerifierResult as VerifierResult, +} from "@trigger.dev/core/v3"; + +export type VerifyInput = { + rawBytes: Uint8Array; + headers: Record; // lower-cased keys, see gotcha + url: string; + secret: string; // plaintext signing secret, fail-closed upstream (never empty here) + nowMs?: number; // injectable clock for deterministic timestamp-tolerance tests +}; + +export interface SchemeVerifier { + readonly scheme: VerifierConfig["scheme"]; + verify(config: VerifierConfig, input: VerifyInput): VerifierResult; +} + +export type { VerifierArtifact, VerifierConfig, VerifierResult }; diff --git a/internal-packages/webhook-engine/src/engine/verification/urlSecret.ts b/internal-packages/webhook-engine/src/engine/verification/urlSecret.ts new file mode 100644 index 00000000000..8ad49011da6 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/urlSecret.ts @@ -0,0 +1,38 @@ +import type { WebhookUrlSecretConfig as UrlSecretConfig } from "@trigger.dev/core/v3"; +import type { SchemeVerifier, VerifierResult, VerifyInput } from "./types.js"; +import { constantTimeEqual } from "./util.js"; +import { deriveIdempotencyKey, tryParseJson } from "./derive.js"; + +export const urlSecretVerifier: SchemeVerifier = { + scheme: "url-secret", + verify(config, input): VerifierResult { + const cfg = config as Extract; + const u = new URL(input.url); + let provided: string | undefined; + if (cfg.placement === "query") { + provided = u.searchParams.get(cfg.paramName) ?? undefined; + } else { + const segs = u.pathname.split("/").filter(Boolean); + provided = segs[segs.length - 1]; + } + if (!provided) + return { ok: false, error: "missing url secret", idempotencyKey: derive0(cfg, input) }; + if (!constantTimeEqual(provided, input.secret)) + return { ok: false, error: "url secret mismatch", idempotencyKey: derive0(cfg, input) }; + return { + ok: true, + idempotencyKey: derive0(cfg, input, provided), + ...tryParseJson(input.rawBytes), + }; + }, +}; + +function derive0(cfg: any, input: VerifyInput, sig = ""): string { + return deriveIdempotencyKey({ + idempotencyField: undefined, + headers: input.headers, + rawBytes: input.rawBytes, + timestampValue: "", + signatureValue: sig, + }); +} diff --git a/internal-packages/webhook-engine/src/engine/verification/util.ts b/internal-packages/webhook-engine/src/engine/verification/util.ts new file mode 100644 index 00000000000..5e2fd3cca7e --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/util.ts @@ -0,0 +1,61 @@ +import { createHmac, timingSafeEqual as nodeTimingSafeEqual, createHash } from "node:crypto"; + +// Constant-time compare. NEVER ===. Both sides first hashed to 32-byte sha256 so +// timingSafeEqual gets equal-length buffers (it throws on length mismatch). +export function constantTimeEqual(a: string, b: string): boolean { + const ha = createHash("sha256").update(a, "utf8").digest(); + const hb = createHash("sha256").update(b, "utf8").digest(); + return nodeTimingSafeEqual(ha, hb); +} + +export function hmacDigest( + algorithm: "sha256" | "sha1", + secret: string | Buffer, + signingBytes: Uint8Array, + encoding: "hex" | "base64" +): string { + return createHmac(algorithm, secret).update(signingBytes).digest(encoding); +} + +// The HMAC key bytes from the stored secret. Most providers use the secret verbatim (utf8); +// Svix-family secrets are `whsec_` → strip the prefix then base64-decode to raw bytes. +export function deriveHmacKey( + secret: string, + transform?: { encoding?: "utf8" | "base64"; stripPrefix?: string } +): Buffer { + let s = secret; + if (transform?.stripPrefix && s.startsWith(transform.stripPrefix)) { + s = s.slice(transform.stripPrefix.length); + } + return Buffer.from(s, transform?.encoding === "base64" ? "base64" : "utf8"); +} + +export function decodeSignature(value: string, encoding: "hex" | "base64"): Buffer { + return Buffer.from(value, encoding); +} + +export function sha256Hex(input: string | Uint8Array): string { + return createHash("sha256").update(input).digest("hex"); +} + +// Build the signing bytes for a templated signing string ("{timestamp}.{body}") +// without ever turning the raw body into a string (preserve exact bytes). +export function buildSigningBytes( + template: string, + rawBytes: Uint8Array, + vars: Record +): Uint8Array { + const BODY = "{body}"; + const idx = template.indexOf(BODY); + const substitute = (s: string) => s.replace(/\{(\w+)\}/g, (_m, k) => vars[k] ?? ""); + if (idx === -1) { + return new TextEncoder().encode(substitute(template)); + } + const head = new TextEncoder().encode(substitute(template.slice(0, idx))); + const tail = new TextEncoder().encode(substitute(template.slice(idx + BODY.length))); + const out = new Uint8Array(head.length + rawBytes.length + tail.length); + out.set(head, 0); + out.set(rawBytes, head.length); + out.set(tail, head.length + rawBytes.length); + return out; +} diff --git a/internal-packages/webhook-engine/src/engine/verification/verify.test.ts b/internal-packages/webhook-engine/src/engine/verification/verify.test.ts new file mode 100644 index 00000000000..f2b0b5fc6a4 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/verification/verify.test.ts @@ -0,0 +1,350 @@ +import { createHmac, generateKeyPairSync, sign as cryptoSign, type KeyObject } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { verify } from "./index.js"; +import type { VerifyInput } from "./types.js"; + +// Real signatures computed with node:crypto, exercised through the config-driven verifier. +// This is the deferred M5 suite, now grounded in the actual provider header shapes. + +const NOW_MS = 1_700_000_000_000; // fixed clock so timestamp-tolerance is deterministic +const NOW_S = Math.floor(NOW_MS / 1000); + +function input( + over: Partial & { rawBytes: Uint8Array; headers: Record } +): VerifyInput { + return { + url: "https://api.example.com/webhooks/v1/ingest/abc", + secret: "whsec_test_secret", + nowMs: NOW_MS, + ...over, + }; +} + +function bytes(s: string): Uint8Array { + return new TextEncoder().encode(s); +} + +const cfg = (config: object) => ({ kind: "config" as const, config: config as any }); + +describe("hmac verifier (config-driven)", () => { + it("verifies a Stripe-style t=,v1= combined header", () => { + const body = JSON.stringify({ id: "evt_123", type: "payment_intent.succeeded" }); + const sig = createHmac("sha256", "whsec_test_secret").update(`${NOW_S}.${body}`).digest("hex"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { source: { from: "signatureField", field: "t" }, toleranceSeconds: 300 }, + signingString: { template: "{timestamp}.{body}" }, + idempotencyField: { from: "body", name: "id" }, + }), + input({ rawBytes: bytes(body), headers: { "stripe-signature": `t=${NOW_S},v1=${sig}` } }) + ); + expect(result.ok).toBe(true); + expect(result.idempotencyKey).toBe("evt_123"); + }); + + it("decodes a form-encoded Slack interactivity body via formPayload", () => { + const interaction = { type: "block_actions", actions: [{ value: "call-1::approve" }] }; + const formBody = `payload=${encodeURIComponent(JSON.stringify(interaction))}`; + const sig = createHmac("sha256", "whsec_test_secret") + .update(`v0:${NOW_S}:${formBody}`) + .digest("hex"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-slack-signature", + signature: { fieldSeparator: "=", field: "v0" }, + timestamp: { + source: { from: "header", name: "x-slack-request-timestamp" }, + toleranceSeconds: 300, + }, + signingString: { template: "v0:{timestamp}:{body}" }, + formPayload: { field: "payload" }, + }), + input({ + rawBytes: bytes(formBody), + headers: { "x-slack-signature": `v0=${sig}`, "x-slack-request-timestamp": String(NOW_S) }, + }) + ); + expect(result.ok).toBe(true); + expect(result.parsedEvent).toEqual(interaction); + }); + + it("rejects a Stripe signature outside the tolerance window", () => { + const body = "{}"; + const staleT = NOW_S - 10_000; + const sig = createHmac("sha256", "whsec_test_secret").update(`${staleT}.${body}`).digest("hex"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { source: { from: "signatureField", field: "t" }, toleranceSeconds: 300 }, + signingString: { template: "{timestamp}.{body}" }, + }), + input({ rawBytes: bytes(body), headers: { "stripe-signature": `t=${staleT},v1=${sig}` } }) + ); + expect(result.ok).toBe(false); + }); + + it("accepts any v1 candidate during Stripe key rotation", () => { + const body = "{}"; + const good = createHmac("sha256", "whsec_test_secret").update(`${NOW_S}.${body}`).digest("hex"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { source: { from: "signatureField", field: "t" }, toleranceSeconds: 300 }, + signingString: { template: "{timestamp}.{body}" }, + }), + input({ + rawBytes: bytes(body), + headers: { "stripe-signature": `t=${NOW_S},v1=deadbeef,v1=${good}` }, + }) + ); + expect(result.ok).toBe(true); + }); + + it("verifies a GitHub-style sha256= prefixed header over the raw body", () => { + const body = JSON.stringify({ action: "opened" }); + const sig = createHmac("sha256", "whsec_test_secret").update(body).digest("hex"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-hub-signature-256", + signature: { fieldSeparator: "=", field: "sha256" }, + signingString: "raw", + idempotencyField: { from: "header", name: "x-github-delivery" }, + }), + input({ + rawBytes: bytes(body), + headers: { "x-hub-signature-256": `sha256=${sig}`, "x-github-delivery": "guid-1" }, + }) + ); + expect(result.ok).toBe(true); + expect(result.idempotencyKey).toBe("guid-1"); + }); + + it("rejects a tampered body", () => { + const sig = createHmac("sha256", "whsec_test_secret").update("{}").digest("hex"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-hub-signature-256", + signature: { fieldSeparator: "=", field: "sha256" }, + signingString: "raw", + }), + input({ + rawBytes: bytes('{"tampered":true}'), + headers: { "x-hub-signature-256": `sha256=${sig}` }, + }) + ); + expect(result.ok).toBe(false); + }); + + it("verifies a Svix-style space-list, base64 secret, {id}.{ts}.{body}", () => { + // Svix secret is whsec_; the HMAC key is the base64-decoded remainder. + const keyBytes = Buffer.from("c2VjcmV0LWtleS1ieXRlcw==", "base64"); + const secret = "whsec_c2VjcmV0LWtleS1ieXRlcw=="; + const id = "msg_123"; + const body = JSON.stringify({ hello: "world" }); + const sig = createHmac("sha256", keyBytes).update(`${id}.${NOW_S}.${body}`).digest("base64"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "base64", + signatureHeader: "svix-signature", + signature: { itemSeparator: " ", fieldSeparator: ",", field: "v1" }, + timestamp: { source: { from: "header", name: "svix-timestamp" }, toleranceSeconds: 300 }, + signingString: { + template: "{id}.{timestamp}.{body}", + vars: { id: { from: "header", name: "svix-id" } }, + }, + secret: { encoding: "base64", stripPrefix: "whsec_" }, + idempotencyField: { from: "header", name: "svix-id" }, + }), + input({ + rawBytes: bytes(body), + secret, + headers: { + "svix-id": id, + "svix-timestamp": String(NOW_S), + "svix-signature": `v1,${sig}`, + }, + }) + ); + expect(result.ok).toBe(true); + expect(result.idempotencyKey).toBe(id); + }); + + it("verifies a Square-style {url}{body} signature", () => { + const url = "https://api.example.com/webhooks/v1/ingest/abc"; + const body = JSON.stringify({ event_id: "sq_1" }); + const sig = createHmac("sha256", "whsec_test_secret").update(`${url}${body}`).digest("base64"); + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "base64", + signatureHeader: "x-square-hmacsha256-signature", + signature: {}, + signingString: { template: "{url}{body}", vars: { url: { from: "url" } } }, + idempotencyField: { from: "body", name: "event_id" }, + }), + input({ rawBytes: bytes(body), url, headers: { "x-square-hmacsha256-signature": sig } }) + ); + expect(result.ok).toBe(true); + expect(result.idempotencyKey).toBe("sq_1"); + }); +}); + +describe("asymmetric verifier (config-driven)", () => { + it("verifies a Discord-style Ed25519 signature with a raw-hex public key", () => { + const { publicKey, privateKey } = generateKeyPairSync("ed25519"); + const rawPubHex = rawEd25519PublicKeyHex(publicKey); + const ts = String(NOW_S); + const body = JSON.stringify({ type: 1 }); + const sig = cryptoSign(null, Buffer.from(`${ts}${body}`), privateKey).toString("hex"); + const config = { + scheme: "asymmetric", + algorithm: "ed25519", + encoding: "hex", + signatureHeader: "x-signature-ed25519", + signature: {}, + timestamp: { source: { from: "header", name: "x-signature-timestamp" } }, + signingString: { template: "{timestamp}{body}" }, + publicKeyEncoding: "raw-hex", + }; + const ok = verify( + cfg(config), + input({ + rawBytes: bytes(body), + secret: rawPubHex, + headers: { "x-signature-ed25519": sig, "x-signature-timestamp": ts }, + }) + ); + expect(ok.ok).toBe(true); + + const tampered = verify( + cfg(config), + input({ + rawBytes: bytes('{"type":2}'), + secret: rawPubHex, + headers: { "x-signature-ed25519": sig, "x-signature-timestamp": ts }, + }) + ); + expect(tampered.ok).toBe(false); + }); + + it("verifies a SendGrid-style ECDSA P-256 signature with an SPKI-DER base64 public key", () => { + const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); + const spkiB64 = publicKey.export({ type: "spki", format: "der" }).toString("base64"); + const ts = String(NOW_S); + const body = JSON.stringify([{ sg_event_id: "e1" }]); + const sig = cryptoSign("sha256", Buffer.from(`${ts}${body}`), privateKey).toString("base64"); + const result = verify( + cfg({ + scheme: "asymmetric", + algorithm: "ecdsa-p256", + encoding: "base64", + signatureHeader: "x-twilio-email-event-webhook-signature", + signature: {}, + timestamp: { source: { from: "header", name: "x-twilio-email-event-webhook-timestamp" } }, + signingString: { template: "{timestamp}{body}" }, + publicKeyEncoding: "spki-der-base64", + }), + input({ + rawBytes: bytes(body), + secret: spkiB64, + headers: { + "x-twilio-email-event-webhook-signature": sig, + "x-twilio-email-event-webhook-timestamp": ts, + }, + }) + ); + expect(result.ok).toBe(true); + }); +}); + +describe("adversarial / fail-closed", () => { + const stripeConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { source: { from: "signatureField", field: "t" }, toleranceSeconds: 300 }, + signingString: { template: "{timestamp}.{body}" }, + }; + + it("rejects a request with no signature header", () => { + const result = verify(cfg(stripeConfig), input({ rawBytes: bytes("{}"), headers: {} })); + expect(result.ok).toBe(false); + }); + + it("rejects when the signature field is absent (timestamp only)", () => { + const result = verify( + cfg(stripeConfig), + input({ rawBytes: bytes("{}"), headers: { "stripe-signature": `t=${NOW_S}` } }) + ); + expect(result.ok).toBe(false); + }); + + it("rejects a body signed with the wrong secret", () => { + const body = "{}"; + const forged = createHmac("sha256", "attacker-secret").update(`${NOW_S}.${body}`).digest("hex"); + const result = verify( + cfg(stripeConfig), + input({ rawBytes: bytes(body), headers: { "stripe-signature": `t=${NOW_S},v1=${forged}` } }) + ); + expect(result.ok).toBe(false); + }); + + it("rejects multiple forged signature candidates (no free pass from the list)", () => { + const result = verify( + cfg(stripeConfig), + input({ + rawBytes: bytes("{}"), + headers: { "stripe-signature": `t=${NOW_S},v1=deadbeef,v1=cafebabe` }, + }) + ); + expect(result.ok).toBe(false); + }); + + it("rejects an empty signature value", () => { + const result = verify( + cfg({ + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-hub-signature-256", + signature: { fieldSeparator: "=", field: "sha256" }, + signingString: "raw", + }), + input({ rawBytes: bytes("{}"), headers: { "x-hub-signature-256": "sha256=" } }) + ); + expect(result.ok).toBe(false); + }); +}); + +// Extract the raw 32-byte Ed25519 public key (last 32 bytes of the SPKI DER) as hex. +function rawEd25519PublicKeyHex(publicKey: KeyObject): string { + const der = publicKey.export({ type: "spki", format: "der" }); + return Buffer.from(der.subarray(der.length - 32)).toString("hex"); +} diff --git a/internal-packages/webhook-engine/src/engine/workerCatalog.ts b/internal-packages/webhook-engine/src/engine/workerCatalog.ts new file mode 100644 index 00000000000..ec6fa8f3bd3 --- /dev/null +++ b/internal-packages/webhook-engine/src/engine/workerCatalog.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { CronSchema, type WorkerCatalog } from "@trigger.dev/redis-worker"; + +export const webhookWorkerCatalog = { + "webhook.deliver": { + schema: z.object({ + deliveryId: z.string(), + createdAt: z.coerce.date(), + }), + visibilityTimeoutMs: 60_000, + retry: { + maxAttempts: 5, + }, + }, + ensurePartitions: { + schema: CronSchema, + visibilityTimeoutMs: 60_000 * 5, + cron: "17 3 * * *", // daily 03:17 default; overridable from options.partitions.ensureSchedule + jitterInMs: 5 * 60_000, // up to 5m jitter; overridable from options.partitions.ensureJitterInMs + retry: { + maxAttempts: 1, // cron jobs do not retry (next tick re-runs) + }, + }, +} satisfies WorkerCatalog; diff --git a/internal-packages/webhook-engine/src/index.ts b/internal-packages/webhook-engine/src/index.ts new file mode 100644 index 00000000000..ba67a006c03 --- /dev/null +++ b/internal-packages/webhook-engine/src/index.ts @@ -0,0 +1,15 @@ +export { WebhookEngine } from "./engine/index.js"; +export { parseFilter, FilterParseError } from "./engine/filter/index.js"; +export { signWithVerifierConfig } from "./engine/signing/index.js"; +export type { SignResult, SignArgs } from "./engine/signing/index.js"; +export { verify } from "./engine/verification/index.js"; +export type { VerifyInput, VerifierResult } from "./engine/verification/index.js"; +export type { + WebhookEngineOptions, + TriggerWebhookTaskParams, + TriggerWebhookTaskCallback, + WebhookDeliverTaskErrorType, + IngestInput, + IngestResult, + ReplayResult, +} from "./engine/types.js"; diff --git a/internal-packages/webhook-engine/test/setup.ts b/internal-packages/webhook-engine/test/setup.ts new file mode 100644 index 00000000000..b2bacd6baf5 --- /dev/null +++ b/internal-packages/webhook-engine/test/setup.ts @@ -0,0 +1,4 @@ +import { vi } from "vitest"; + +// Set extended timeout for container tests +vi.setConfig({ testTimeout: 60_000 }); diff --git a/internal-packages/webhook-engine/tsconfig.build.json b/internal-packages/webhook-engine/tsconfig.build.json new file mode 100644 index 00000000000..89c87a3dc67 --- /dev/null +++ b/internal-packages/webhook-engine/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"], + "compilerOptions": { + "composite": true, + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], + "outDir": "dist", + "module": "Node16", + "moduleResolution": "Node16", + "moduleDetection": "force", + "verbatimModuleSyntax": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "declaration": true + } +} diff --git a/internal-packages/webhook-engine/tsconfig.json b/internal-packages/webhook-engine/tsconfig.json new file mode 100644 index 00000000000..af630abe1f1 --- /dev/null +++ b/internal-packages/webhook-engine/tsconfig.json @@ -0,0 +1,8 @@ +{ + "references": [{ "path": "./tsconfig.src.json" }, { "path": "./tsconfig.test.json" }], + "compilerOptions": { + "moduleResolution": "Node16", + "module": "Node16", + "customConditions": ["@triggerdotdev/source"] + } +} diff --git a/internal-packages/webhook-engine/tsconfig.src.json b/internal-packages/webhook-engine/tsconfig.src.json new file mode 100644 index 00000000000..0df3d2d222f --- /dev/null +++ b/internal-packages/webhook-engine/tsconfig.src.json @@ -0,0 +1,20 @@ +{ + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "src/**/*.test.ts"], + "compilerOptions": { + "composite": true, + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], + "module": "Node16", + "moduleResolution": "Node16", + "moduleDetection": "force", + "verbatimModuleSyntax": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "customConditions": ["@triggerdotdev/source"] + } +} diff --git a/internal-packages/webhook-engine/tsconfig.test.json b/internal-packages/webhook-engine/tsconfig.test.json new file mode 100644 index 00000000000..f4b838bd2e1 --- /dev/null +++ b/internal-packages/webhook-engine/tsconfig.test.json @@ -0,0 +1,20 @@ +{ + "include": ["src/**/*.test.ts"], + "references": [{ "path": "./tsconfig.src.json" }], + "compilerOptions": { + "composite": true, + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], + "module": "Node16", + "moduleResolution": "Node16", + "moduleDetection": "force", + "verbatimModuleSyntax": false, + "types": ["vitest/globals"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "preserveWatchOutput": true, + "strict": true, + "customConditions": ["@triggerdotdev/source"] + } +} diff --git a/internal-packages/webhook-engine/vitest.config.ts b/internal-packages/webhook-engine/vitest.config.ts new file mode 100644 index 00000000000..daafd294fa8 --- /dev/null +++ b/internal-packages/webhook-engine/vitest.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vitest/config"; +import { DurationShardingSequencer } from "@internal/testcontainers/sequencer"; + +export default defineConfig({ + test: { + sequence: { sequencer: DurationShardingSequencer }, + globals: true, + retry: process.env.CI ? 2 : 0, + environment: "node", + setupFiles: ["./test/setup.ts"], + testTimeout: 30000, + hookTimeout: 30000, + }, + esbuild: { + target: "node18", + }, +}); diff --git a/internal-packages/webhook-sources/NOTICE.md b/internal-packages/webhook-sources/NOTICE.md new file mode 100644 index 00000000000..e127789978e --- /dev/null +++ b/internal-packages/webhook-sources/NOTICE.md @@ -0,0 +1,11 @@ +# Third-party attribution + +The bundled provider samples in `src/generated/hookdeck-samples.json` are ingested from the +[Hookdeck webhook-samples](https://github.com/hookdeck/webhook-samples) dataset, which is licensed +under the MIT License. + +The event bodies are normalized (transport and signature headers stripped, routing headers kept) +into this package's `SampleRecord` shape by `src/ingest.ts`. Signatures are not stored; the console +signs each body with the target endpoint's own config at send time. + +To refresh the snapshot, run `pnpm run ingest`. diff --git a/internal-packages/webhook-sources/catalog/build-aggregates.ts b/internal-packages/webhook-sources/catalog/build-aggregates.ts new file mode 100644 index 00000000000..939f4f254b5 --- /dev/null +++ b/internal-packages/webhook-sources/catalog/build-aggregates.ts @@ -0,0 +1,52 @@ +import { readdirSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const srcDir = fileURLToPath(new URL("../src/", import.meta.url)); + +const alias = (base: string) => base.replace(/[^a-zA-Z0-9]/g, "_"); + +const listModules = (dir: string, exclude: string[]) => + readdirSync(new URL(`../src/${dir}/`, import.meta.url)) + .filter((f) => f.endsWith(".ts") && !exclude.includes(f)) + .map((f) => f.replace(/\.ts$/, "")) + .sort(); + +const banner = "/** GENERATED by catalog/build-aggregates.ts. Do not edit by hand. */"; + +const registryModules = listModules("registry", ["types.ts", "index.ts"]); +const registryImports = registryModules + .map((m) => `import { entry as e_${alias(m)} } from "./${m}.js";`) + .join("\n"); +const registryList = registryModules.map((m) => `e_${alias(m)}`).join(", "); +const registryOut = `${banner} +import { type ProviderRegistryEntry } from "./types.js"; +${registryImports} + +export const registryEntries: ProviderRegistryEntry[] = [${registryList}]; + +export const registry: Record = Object.fromEntries( + registryEntries.map((e) => [e.id, e]) +); + +export function getProvider(id: string): ProviderRegistryEntry | undefined { + return registry[id]; +} +`; +writeFileSync(new URL("./registry/index.ts", `file://${srcDir}`), registryOut); + +const handAuthoredModules = listModules("handAuthored", ["index.ts"]); +const handImports = handAuthoredModules + .map((m) => `import { samples as s_${alias(m)} } from "./${m}.js";`) + .join("\n"); +const handSpread = handAuthoredModules.map((m) => `...s_${alias(m)}`).join(", "); +const handOut = `${banner} +import { type SampleRecord } from "../sampleRecord.js"; +${handImports} + +export const handAuthoredSamples: SampleRecord[] = [${handSpread}]; +`; +writeFileSync(new URL("./handAuthored/index.ts", `file://${srcDir}`), handOut); + +console.log( + `Aggregated ${registryModules.length} registry entries, ${handAuthoredModules.length} hand-authored sample modules` +); diff --git a/internal-packages/webhook-sources/catalog/build-brief.md b/internal-packages/webhook-sources/catalog/build-brief.md new file mode 100644 index 00000000000..4ea5a7f9919 --- /dev/null +++ b/internal-packages/webhook-sources/catalog/build-brief.md @@ -0,0 +1,118 @@ +# Provider build brief (read this fully, then build your assigned provider) + +You are building ONE provider for the webhook sample library. Do the work YOURSELF. Do NOT spawn or +delegate to any subagents. Only create your provider's own files; do NOT edit any shared file +(`providers.json`, any `index.ts`, `samples.ts`, or another provider's files). + +PACKAGE: `/Users/eric/code/triggerdotdev/isolated/webhooks/internal-packages/webhook-sources` + +## What to create + +Find your provider's row in the FACTS table below. Then: + +**A registry entry, always:** `src/registry/.ts` exporting `entry` (see clerk reference). + +**Sample bodies, depending on `source`:** +- `source=hookdeck` -> samples already exist in `src/generated/hookdeck-samples.json`. Do NOT author a + sample file. Just read that JSON, confirm entries for your provider, and report count + eventTypes. +- `source=handauthored` -> create `src/handAuthored/.ts` exporting `samples: SampleRecord[]` with + 4 to 6 representative events. USE WEB SEARCH against the docs URL to get the REAL payload shape. + +## Reference (clerk) + +`src/registry/clerk.ts`: +``` +import { type ProviderRegistryEntry } from "./types.js"; +export const entry: ProviderRegistryEntry = { + id: "clerk", label: "Clerk", category: "auth-identity", + docsUrl: "https://clerk.com/docs/integrations/webhooks/overview", + preset: "svix", secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, sampleSource: "handauthored", +}; +``` +`src/handAuthored/clerk.ts` exports `export const samples: SampleRecord[] = [ { provider, providerLabel, presetId, eventType, name, description?, body, docsUrl?, provenance } ]`. + +## Shapes + +`ProviderRegistryEntry`: `id, label, category, icon?, docsUrl?, preset?, secretProvisioning ("provider"|"integrator"|"either"), eventTypeSource ({from:"body",path} | {from:"header",name}), sampleSource ("hookdeck"|"octokit"|"capture"|"handauthored")`. + +`SampleRecord`: `provider (string), providerLabel?, presetId? (one of stripe|github|svix|square|discord), eventType (string), name (string), description?, body (the event JSON), extraHeaders? (record of NON-signature routing headers), docsUrl?, provenance ({kind:"handauthored", snapshotDate:"2026-07"})`. + +## Preset verification (CRITICAL: verify before you set `preset`/`presetId`) + +Your row lists a preset HYPOTHESIS. Set `entry.preset` and every sample's `presetId` to it ONLY if the +provider's real signature wire format EXACTLY matches one of ours below. If it does not match, DROP the +preset: omit `entry.preset`, omit `presetId`, keep `sampleSource`, and report that you downgraded to +sample-only and why. + +- `stripe`: header `stripe-signature` = `t=,v1=`, HMAC-SHA256 over `{t}.{rawBody}`. +- `github`: header `x-hub-signature-256` = `sha256=`, HMAC-SHA256 over `rawBody`. (A provider using + `x-hub-signature` WITHOUT the `-256`, or SHA1, does NOT match.) +- `svix` (Standard Webhooks): headers `webhook-id`/`webhook-timestamp`/`webhook-signature` (or the + `svix-*` equivalents), HMAC-SHA256 base64 over `{id}.{timestamp}.{rawBody}`, secret `whsec_`-prefixed. +- `square`: header `x-square-hmacsha256-signature`, HMAC-SHA256 base64 over `{notificationUrl}{rawBody}`. +- `discord`: headers `X-Signature-Ed25519` + `X-Signature-Timestamp`, Ed25519 over `{timestamp}{rawBody}`. + +## Rules + +- `eventType` MUST equal the real discriminant value for that sample (from the row's `discriminant`). +- **Header-discriminated providers** (row says `discriminant=header:`): the body has no type field, + so add `extraHeaders: { "": "" }` to each sample so the type survives loading. + Body-discriminated providers must NOT set signature headers in `extraHeaders` (the composer signs at + send time; never store a signature). +- Some providers deliver an ARRAY of events (e.g. SendGrid) or a nested/wrapped shape (row notes it); + model the body faithfully and set `eventType` to the meaningful value. Some are form-encoded (Twilio): + represent the body as the key/value object. +- Use realistic-but-FAKE ids/values; no real secrets or PII. +- This repo BLOCKS new `//` line comments via a hook. Use `/** */` JSDoc only, or none. If a Write is + rejected for comments, remove them and retry. +- Import paths end in `.js`. Do NOT run tests, aggregate, or typecheck; do NOT touch index files. + +## Report format (final message, plain text) + +`FILES: . PRESET: . EVENTS: . SOURCE: . CAVEATS: .` + +## FACTS (one row per provider) + +Columns: id | label | category | tier | preset-hypothesis | scheme | discriminant | secret | source | docsUrl | suggested events + +- stripe | Stripe | payments | first-class | stripe | hmac-sha256 t=,v1= | body:type | integrator | hookdeck | https://docs.stripe.com/webhooks | (registry only; ~13 hookdeck samples already tagged presetId=stripe) +- anthropic | Anthropic (Claude) | ai-platform | first-class | svix | standard-webhooks | body:type | provider | handauthored | https://platform.claude.com/docs/en/managed-agents/webhooks | session.status_idled, session.run_started, deployment_run.succeeded, deployment_run.failed, agent.* (Managed Agents; envelope {id,type,created_at,data}) +- resend | Resend | email | first-class | svix | svix | body:type | provider | handauthored | https://resend.com/docs/dashboard/webhooks/introduction | email.sent, email.delivered, email.bounced, email.complained, email.opened, email.clicked (body {type, created_at, data}) +- replicate | Replicate | ai-platform | first-class | svix | standard-webhooks | body:status | provider | handauthored | https://replicate.com/docs/topics/webhooks/verify-webhook | prediction succeeded/failed/canceled (body = prediction object {id, status, output, ...}); pick eventType from status +- brex | Brex | fintech | first-class | svix | svix | body:type | provider | handauthored | https://developer.brex.com/docs/webhooks/ | card transaction/expense/budget events per docs +- recall-ai | Recall.ai | ai-platform | first-class | svix | standard-webhooks | body:event | provider | handauthored | https://docs.recall.ai/docs/authenticating-requests-from-recallai | bot.status_change, recording.done, transcript.done (body {event, data}) +- square | Square | payments | first-class | square | hmac-sha256 (url+body) | body:type | integrator | handauthored | https://developer.squareup.com/docs/webhooks/step3validate | payment.created, payment.updated, order.created, refund.created (body {merchant_id, type, event_id, created_at, data}) +- discord | Discord | communication | first-class | discord | ed25519 | body:type | provider | handauthored | https://docs.discord.com/developers/events/webhook-events | Webhook Events (type 0 with body.event.type e.g. APPLICATION_AUTHORIZED) and/or Interactions PING (type 1); numeric top-level type, verify carefully and note in caveats +- gitlab | GitLab | source-control | first-class | svix (VERIFY signing-token) | default plaintext X-Gitlab-Token OR opt-in signing-token | header:x-gitlab-event (also body.object_kind) | integrator | hookdeck | https://docs.gitlab.com/user/project/integrations/webhooks/ | 7 untagged hookdeck samples exist. If GitLab's signing-token is standard-webhooks-compatible, hand-author 3-4 svix-tagged samples; else register sample-only (drop preset) and rely on hookdeck samples +- whatsapp | WhatsApp (Meta) | communication | first-class | github (VERIFY x-hub-signature-256) | hmac-sha256 x-hub-signature-256 | body (entry[].changes[].value.field: messages|statuses) | integrator | handauthored | https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks/ | inbound message, message status (deep nested; set eventType to the change field like "messages"/"statuses"); verify header is x-hub-signature-256 sha256= +- jira | Jira | pm | first-class | github (VERIFY: Jira uses x-hub-signature WITHOUT -256, likely NO match) | hmac-sha256 x-hub-signature | body:webhookEvent | integrator | handauthored | https://developer.atlassian.com/cloud/jira/platform/webhooks/ | jira:issue_created, jira:issue_updated, comment_created (body {webhookEvent, issue, ...}); if header is x-hub-signature (no -256) it does NOT match our github preset -> downgrade to sample-only +- twilio | Twilio | communication | sample-only | none | hmac-sha1 | body (form-encoded; no type field) | integrator | handauthored | https://www.twilio.com/docs/usage/webhooks/webhooks-security | inbound SMS and call-status; body is a key/value object of form params; set eventType like "message.inbound" / "call.status-callback" +- telegram | Telegram | communication | sample-only | none | shared-secret | body (Update object; key present = type, e.g. message/callback_query) | integrator | handauthored | https://core.telegram.org/bots/api#setwebhook | message, edited_message, callback_query, inline_query; eventType = the update field name +- zoom | Zoom | communication | sample-only | none | hmac-sha256 | body:event | integrator | handauthored | https://developers.zoom.us/docs/api/webhooks/ | meeting.started, meeting.ended, recording.completed, meeting.participant_joined (body {event, payload}) +- sendgrid | SendGrid | email | sample-only | none | ecdsa | body[]:event (ARRAY of events) | provider | handauthored | https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features | delivered, open, click, bounce, spamreport; body is an ARRAY; each sample = array with one representative event, eventType = that event value +- postmark | Postmark | email | sample-only | none | basic | body:RecordType | integrator | handauthored | https://postmarkapp.com/developer/webhooks/inbound-webhook | Delivery, Bounce, SpamComplaint, Open, Inbound (body {RecordType, ...}) +- zendesk | Zendesk | support | sample-only | none | hmac-sha256 | body (trigger-defined; use a common ticket shape) | integrator | handauthored | https://developer.zendesk.com/documentation/webhooks/verifying/ | ticket.created, ticket.updated (payload is configurable; use a representative ticket event JSON) +- intercom | Intercom | support | sample-only | none | hmac-sha1 | body:topic | integrator | handauthored | https://developers.intercom.com/docs/references/webhooks/webhook-models | conversation.user.created, conversation.user.replied, contact.created (body {type:"notification_event", topic, data}) +- sentry | Sentry | observability | sample-only | none | hmac-sha256 | header:sentry-hook-resource (+ body.action) | integrator | handauthored | https://docs.sentry.io/product/integrations/integration-platform/webhooks/ | issue.created, issue.resolved, error.created (header sentry-hook-resource; body {action, data}); header-discriminated -> add extraHeaders +- pagerduty | PagerDuty | observability | sample-only | none | hmac-sha256 | body:event.event_type | integrator | handauthored | https://developer.pagerduty.com/docs/webhooks/webhook-signatures/ | incident.triggered, incident.acknowledged, incident.resolved (body {event:{event_type, data}}) +- vercel | Vercel | hosting-infra | sample-only | none | hmac-sha1 | body:type | integrator | handauthored | https://vercel.com/docs/webhooks | deployment.created, deployment.succeeded, deployment.error, deployment.canceled (body {type, id, payload}) +- linear | Linear | pm | sample-only | none | hmac-sha256 | body:type (+ body.action) | integrator | handauthored | https://linear.app/developers/webhooks | Issue create, Comment create (body {action, type, data, ...}) +- auth0 | Auth0 | auth-identity | sample-only | none | bearer | body[]:data.type (log-stream events array) | integrator | handauthored | https://auth0.com/docs/customize/log-streams/custom-log-streams | successful login (s), failed login (f), signup (ss); body is an array of log events; eventType from the log event type +- workos | WorkOS | auth-identity | sample-only | none | hmac-sha256 | body:event | provider | handauthored | https://workos.com/docs/events/data-syncing/webhooks | dsync.user.created, connection.activated, user.created (body {id, event, data}) +- supabase | Supabase | hosting-infra | sample-only | none | shared-secret | body:type (INSERT/UPDATE/DELETE) | integrator | handauthored | https://supabase.com/docs/guides/database/webhooks | INSERT, UPDATE, DELETE (body {type, table, schema, record, old_record}) +- notion | Notion | productivity | sample-only | none | hmac-sha256 | body:type | provider | handauthored | https://developers.notion.com/reference/webhooks | page.content_updated, page.created, database.content_updated (body {type, ...}); verify current event type names via docs +- attio | Attio | crm | sample-only | none | hmac-sha256 | body:events[].event_type | integrator | handauthored | https://docs.attio.com/rest-api/guides/webhooks | record.created, record.updated, list-entry.created (body {events:[{event_type, id}], webhook_id}) +- close-crm | Close | crm | sample-only | none | hmac-sha256 | body:event.action (+ object_type) | provider | handauthored | https://developer.close.com/topics/webhooks/ | lead.created, activity.created, opportunity.status_change (body {event:{action, object_type, data}}) +- hubspot | HubSpot | crm | sample-only | none | hmac-sha256 v3 | body[]:subscriptionType (ARRAY) | provider | hookdeck | https://developers.hubspot.com/docs/guides/api/app-management/webhooks | (registry only; ~3 hookdeck samples exist; report them) +- calendly | Calendly | calendar-scheduling | sample-only | none | hmac-sha256 | body:event | provider | handauthored | https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures | invitee.created, invitee.canceled (body {event, created_at, payload}) +- cal-com | Cal.com | calendar-scheduling | sample-only | none | hmac-sha256 | body:triggerEvent | integrator | handauthored | https://cal.com/docs/developing/guides/automation/webhooks | BOOKING_CREATED, BOOKING_CANCELLED, BOOKING_RESCHEDULED, MEETING_ENDED (body {triggerEvent, createdAt, payload}) +- typeform | Typeform | forms | sample-only | none | hmac-sha256 | body:event_type | integrator | handauthored | https://www.typeform.com/developers/webhooks/secure-your-webhooks/ | form_response (body {event_id, event_type, form_response}) +- docusign | DocuSign | e-signature | sample-only | none | hmac-sha256 | body:event | integrator | handauthored | https://developers.docusign.com/platform/webhooks/connect/ | envelope-completed, envelope-sent, recipient-completed (Connect JSON body {event, apiVersion, data}) +- shopify | Shopify | commerce | sample-only | none | hmac-sha256 | header:x-shopify-topic | integrator | hookdeck | https://shopify.dev/docs/apps/build/webhooks/verify-deliveries | (registry only; ~181 hookdeck samples; header-discriminated, confirm samples carry x-shopify-topic) +- plaid | Plaid | fintech | sample-only | none | jwt es256 | body:webhook_type (+ webhook_code) | provider | handauthored | https://plaid.com/docs/api/webhooks/webhook-verification/ | TRANSACTIONS/SYNC_UPDATES_AVAILABLE, ITEM/ERROR, ITEM/LOGIN_REQUIRED (body {webhook_type, webhook_code, item_id, ...}); set eventType like "TRANSACTIONS.SYNC_UPDATES_AVAILABLE" +- elevenlabs | ElevenLabs | voice | sample-only | none | hmac-sha256 | body:type | provider | handauthored | https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks | post_call_transcription, post_call_audio, call_initiation_failure (body {type, event_timestamp, data}) +- vapi | Vapi | voice | sample-only | none | hmac-sha256 | body:message.type | integrator | handauthored | https://docs.vapi.ai/server-url/server-authentication | end-of-call-report, status-update, transcript, tool-calls (body {message:{type, ...}}) +- retell | Retell AI | voice | sample-only | none | hmac-sha256 | body:event | provider | handauthored | https://docs.retellai.com/features/secure-webhook | call_started, call_ended, call_analyzed (body {event, call}) +- assemblyai | AssemblyAI | ai-platform | sample-only | none | shared-secret | body:status | integrator | handauthored | https://www.assemblyai.com/docs/concepts/webhooks | transcript completed, error (body {transcript_id, status}) +- deepgram | Deepgram | ai-platform | sample-only | none | none-or-ip-allowlist | body (async transcript result; no type field) | integrator | handauthored | https://developers.deepgram.com/docs/callback | transcript ready (body = Deepgram results object); set eventType like "transcript.completed" diff --git a/internal-packages/webhook-sources/catalog/build-v1.ts b/internal-packages/webhook-sources/catalog/build-v1.ts new file mode 100644 index 00000000000..7bf2bb25ac1 --- /dev/null +++ b/internal-packages/webhook-sources/catalog/build-v1.ts @@ -0,0 +1,519 @@ +import { writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +type Row = [ + id: string, + label: string, + category: string, + preset: string | null, + scheme: string, + popularity: string, + sampleSource: string, + docsUrl: string, +]; + +const PRESETS = new Set(["stripe", "github", "svix", "square", "discord"]); + +const DOD: Record = { + "first-class": ["registryEntry", "samples", "roundTrip", "producer"], + "sample-only": ["registryEntry", "samples"], +}; + +const rows: Row[] = [ + [ + "openai", + "OpenAI", + "ai-platform", + "svix", + "standard-webhooks", + "high", + "handauthored", + "https://platform.openai.com/docs/guides/webhooks", + ], + [ + "anthropic", + "Anthropic (Claude)", + "ai-platform", + "svix", + "standard-webhooks", + "high", + "handauthored", + "https://platform.claude.com/docs/en/managed-agents/webhooks", + ], + [ + "replicate", + "Replicate", + "ai-platform", + "svix", + "standard-webhooks", + "medium", + "handauthored", + "https://replicate.com/docs/topics/webhooks/verify-webhook", + ], + [ + "recall-ai", + "Recall.ai", + "ai-platform", + "svix", + "standard-webhooks", + "medium", + "handauthored", + "https://docs.recall.ai/docs/authenticating-requests-from-recallai", + ], + [ + "clerk", + "Clerk", + "auth-identity", + "svix", + "svix", + "high", + "handauthored", + "https://clerk.com/docs/guides/development/webhooks/overview", + ], + [ + "resend", + "Resend", + "email", + "svix", + "svix", + "high", + "handauthored", + "https://resend.com/docs/dashboard/webhooks/introduction", + ], + [ + "brex", + "Brex", + "fintech", + "svix", + "svix", + "medium", + "handauthored", + "https://developer.brex.com/docs/webhooks/", + ], + [ + "gitlab", + "GitLab", + "source-control", + "svix", + "standard-webhooks", + "high", + "hookdeck", + "https://docs.gitlab.com/user/project/integrations/webhooks/", + ], + [ + "github", + "GitHub", + "source-control", + "github", + "hmac-sha256", + "high", + "hookdeck", + "https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries", + ], + [ + "whatsapp", + "WhatsApp (Meta)", + "communication", + "github", + "hmac-sha256", + "high", + "handauthored", + "https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks/", + ], + [ + "jira", + "Jira", + "pm", + "github", + "hmac-sha256", + "high", + "handauthored", + "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + ], + [ + "stripe", + "Stripe", + "payments", + "stripe", + "stripe", + "high", + "hookdeck", + "https://docs.stripe.com/webhooks", + ], + [ + "square", + "Square", + "payments", + "square", + "hmac-sha256", + "medium", + "handauthored", + "https://developer.squareup.com/docs/webhooks/step3validate", + ], + [ + "discord", + "Discord", + "communication", + "discord", + "ed25519", + "high", + "handauthored", + "https://docs.discord.com/developers/events/webhook-events", + ], + + [ + "elevenlabs", + "ElevenLabs", + "voice", + null, + "hmac-sha256", + "high", + "handauthored", + "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + ], + [ + "vapi", + "Vapi", + "voice", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://docs.vapi.ai/server-url/server-authentication", + ], + [ + "retell", + "Retell AI", + "voice", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://docs.retellai.com/features/secure-webhook", + ], + [ + "assemblyai", + "AssemblyAI", + "ai-platform", + null, + "shared-secret", + "medium", + "handauthored", + "https://www.assemblyai.com/docs/concepts/webhooks", + ], + [ + "deepgram", + "Deepgram", + "ai-platform", + null, + "none-or-ip-allowlist", + "medium", + "handauthored", + "https://developers.deepgram.com/docs/callback", + ], + [ + "slack", + "Slack", + "communication", + null, + "hmac-sha256", + "high", + "handauthored", + "https://docs.slack.dev/authentication/verifying-requests-from-slack/", + ], + [ + "telegram", + "Telegram", + "communication", + null, + "shared-secret", + "medium", + "handauthored", + "https://core.telegram.org/bots/api#setwebhook", + ], + [ + "twilio", + "Twilio", + "communication", + null, + "hmac-sha1", + "high", + "handauthored", + "https://www.twilio.com/docs/usage/webhooks/webhooks-security", + ], + [ + "zoom", + "Zoom", + "communication", + null, + "hmac-sha256", + "high", + "handauthored", + "https://developers.zoom.us/docs/api/webhooks/", + ], + [ + "sendgrid", + "SendGrid", + "email", + null, + "ecdsa", + "high", + "handauthored", + "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features", + ], + [ + "postmark", + "Postmark", + "email", + null, + "basic", + "medium", + "handauthored", + "https://postmarkapp.com/developer/webhooks/inbound-webhook", + ], + [ + "zendesk", + "Zendesk", + "support", + null, + "hmac-sha256", + "high", + "handauthored", + "https://developer.zendesk.com/documentation/webhooks/verifying/", + ], + [ + "intercom", + "Intercom", + "support", + null, + "hmac-sha1", + "high", + "handauthored", + "https://developers.intercom.com/docs/references/webhooks/webhook-models", + ], + [ + "cal-com", + "Cal.com", + "calendar-scheduling", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://cal.com/docs/developing/guides/automation/webhooks", + ], + [ + "calendly", + "Calendly", + "calendar-scheduling", + null, + "hmac-sha256", + "high", + "handauthored", + "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + ], + [ + "linear", + "Linear", + "pm", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://linear.app/developers/webhooks", + ], + [ + "sentry", + "Sentry", + "observability", + null, + "hmac-sha256", + "high", + "handauthored", + "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + ], + [ + "vercel", + "Vercel", + "hosting-infra", + null, + "hmac-sha1", + "high", + "handauthored", + "https://vercel.com/docs/webhooks", + ], + [ + "pagerduty", + "PagerDuty", + "observability", + null, + "hmac-sha256", + "high", + "handauthored", + "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + ], + [ + "auth0", + "Auth0", + "auth-identity", + null, + "bearer", + "high", + "handauthored", + "https://auth0.com/docs/customize/log-streams/custom-log-streams", + ], + [ + "workos", + "WorkOS", + "auth-identity", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://workos.com/docs/events/data-syncing/webhooks", + ], + [ + "supabase", + "Supabase", + "hosting-infra", + null, + "shared-secret", + "high", + "handauthored", + "https://supabase.com/docs/guides/database/webhooks", + ], + [ + "notion", + "Notion", + "productivity", + null, + "hmac-sha256", + "high", + "handauthored", + "https://developers.notion.com/reference/webhooks", + ], + [ + "attio", + "Attio", + "crm", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://docs.attio.com/rest-api/guides/webhooks", + ], + [ + "close-crm", + "Close", + "crm", + null, + "hmac-sha256", + "medium", + "handauthored", + "https://developer.close.com/topics/webhooks/", + ], + [ + "hubspot", + "HubSpot", + "crm", + null, + "hmac-sha256", + "high", + "hookdeck", + "https://developers.hubspot.com/docs/guides/api/app-management/webhooks", + ], + [ + "typeform", + "Typeform", + "forms", + null, + "hmac-sha256", + "high", + "handauthored", + "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + ], + [ + "docusign", + "DocuSign", + "e-signature", + null, + "hmac-sha256", + "high", + "handauthored", + "https://developers.docusign.com/platform/webhooks/connect/", + ], + [ + "shopify", + "Shopify", + "commerce", + null, + "hmac-sha256", + "high", + "hookdeck", + "https://shopify.dev/docs/apps/build/webhooks/verify-deliveries", + ], + [ + "plaid", + "Plaid", + "fintech", + null, + "jwt", + "high", + "handauthored", + "https://plaid.com/docs/api/webhooks/webhook-verification/", + ], +]; + +const popRank: Record = { high: 0, "medium-high": 1, medium: 2, low: 3 }; + +const providers = rows.map( + ([id, label, category, preset, scheme, popularity, sampleSource, docsUrl]) => { + const tier = preset ? "first-class" : "sample-only"; + const checklist = Object.fromEntries(DOD[tier].map((k) => [k, false])); + return { + id, + label, + category, + tier, + preset, + signatureScheme: scheme, + aiRelevance: "high", + popularity, + sampleSource, + docsUrl, + status: "not-started", + owner: null, + checklist, + notes: "", + }; + } +); + +providers.sort((a, b) => { + const t = (a.tier === "first-class" ? 0 : 1) - (b.tier === "first-class" ? 0 : 1); + if (t !== 0) return t; + const p = (popRank[a.popularity] ?? 9) - (popRank[b.popularity] ?? 9); + if (p !== 0) return p; + return a.label.localeCompare(b.label); +}); + +const seen = new Set(); +for (const p of providers) { + if (seen.has(p.id)) throw new Error(`duplicate provider id: ${p.id}`); + seen.add(p.id); + if (p.preset && !PRESETS.has(p.preset)) throw new Error(`unknown preset ${p.preset} on ${p.id}`); +} + +const catalog = { + release: "webhooks-v1", + scope: "ai-agent-first", + updatedAt: null, + definitionOfDone: DOD, + statusValues: ["not-started", "in-progress", "complete", "blocked"], + providers, +}; + +const outPath = fileURLToPath(new URL("./providers.json", import.meta.url)); +writeFileSync(outPath, JSON.stringify(catalog, null, 2) + "\n"); +console.log(`Wrote ${providers.length} providers to providers.json`); +console.log(` first-class: ${providers.filter((p) => p.tier === "first-class").length}`); +console.log(` sample-only: ${providers.filter((p) => p.tier === "sample-only").length}`); diff --git a/internal-packages/webhook-sources/catalog/mark.ts b/internal-packages/webhook-sources/catalog/mark.ts new file mode 100644 index 00000000000..1f1047532fb --- /dev/null +++ b/internal-packages/webhook-sources/catalog/mark.ts @@ -0,0 +1,76 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const args = process.argv.slice(2); +const id = args[0]; +if (!id || id.startsWith("--")) { + console.error( + "usage: mark.ts [--status ] [--check k1,k2,...] [--owner ] [--note ]" + ); + process.exit(1); +} + +const flag = (name: string) => { + const i = args.indexOf(`--${name}`); + return i === -1 ? undefined : args[i + 1]; +}; + +const path = fileURLToPath(new URL("./providers.json", import.meta.url)); +const catalog = JSON.parse(readFileSync(path, "utf8")); +const provider = catalog.providers.find((p: { id: string }) => p.id === id); +if (!provider) { + console.error(`provider not found: ${id}`); + process.exit(1); +} + +const status = flag("status"); +if (status) { + if (!catalog.statusValues.includes(status)) { + console.error(`invalid status ${status}; one of ${catalog.statusValues.join(", ")}`); + process.exit(1); + } + provider.status = status; +} + +const newTier = flag("tier"); +if (newTier) { + const keys: string[] | undefined = catalog.definitionOfDone[newTier]; + if (!keys) { + console.error( + `invalid tier ${newTier}; one of ${Object.keys(catalog.definitionOfDone).join(", ")}` + ); + process.exit(1); + } + provider.tier = newTier; + provider.checklist = Object.fromEntries(keys.map((k) => [k, provider.checklist[k] ?? false])); +} + +const newPreset = flag("preset"); +if (newPreset !== undefined) provider.preset = newPreset === "null" ? null : newPreset; + +const check = flag("check"); +if (check) { + const dodKeys: string[] = catalog.definitionOfDone[provider.tier] ?? []; + for (const key of check + .split(",") + .map((s) => s.trim()) + .filter(Boolean)) { + if (!dodKeys.includes(key)) { + console.error(`checklist key '${key}' not in ${provider.tier} DoD (${dodKeys.join(", ")})`); + process.exit(1); + } + provider.checklist[key] = true; + } +} + +const owner = flag("owner"); +if (owner !== undefined) provider.owner = owner === "null" ? null : owner; + +const note = flag("note"); +if (note !== undefined) provider.notes = note; + +writeFileSync(path, JSON.stringify(catalog, null, 2) + "\n"); +const done = Object.values(provider.checklist).filter(Boolean).length; +console.log( + `${id}: status=${provider.status} checklist=${done}/${Object.keys(provider.checklist).length} owner=${provider.owner ?? "-"}` +); diff --git a/internal-packages/webhook-sources/catalog/providers.json b/internal-packages/webhook-sources/catalog/providers.json new file mode 100644 index 00000000000..88bbda1640f --- /dev/null +++ b/internal-packages/webhook-sources/catalog/providers.json @@ -0,0 +1,925 @@ +{ + "release": "webhooks-v1", + "scope": "ai-agent-first", + "updatedAt": null, + "definitionOfDone": { + "first-class": [ + "registryEntry", + "samples", + "roundTrip", + "producer" + ], + "sample-only": [ + "registryEntry", + "samples" + ] + }, + "statusValues": [ + "not-started", + "in-progress", + "complete", + "blocked" + ], + "providers": [ + { + "id": "anthropic", + "label": "Anthropic (Claude)", + "category": "ai-platform", + "tier": "sample-only", + "preset": null, + "signatureScheme": "standard-webhooks", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://platform.claude.com/docs/en/managed-agents/webhooks", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "downgraded: single X-Webhook-Signature header, not 3-header svix scheme" + }, + { + "id": "clerk", + "label": "Clerk", + "category": "auth-identity", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "svix", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://clerk.com/docs/guides/development/webhooks/overview", + "status": "complete", + "owner": null, + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "discord", + "label": "Discord", + "category": "communication", + "tier": "first-class", + "preset": "discord", + "signatureScheme": "ed25519", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://docs.discord.com/developers/events/webhook-events", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "ed25519 asymmetric: preset exact-match verified structurally, not sign/verify round-tripped" + }, + { + "id": "github", + "label": "GitHub", + "category": "source-control", + "tier": "first-class", + "preset": "github", + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "hookdeck", + "docsUrl": "https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries", + "status": "complete", + "owner": null, + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "gitlab", + "label": "GitLab", + "category": "source-control", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "standard-webhooks", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "hookdeck", + "docsUrl": "https://docs.gitlab.com/user/project/integrations/webhooks/", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "jira", + "label": "Jira", + "category": "pm", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "downgraded: x-hub-signature (no -256) != github preset" + }, + { + "id": "openai", + "label": "OpenAI", + "category": "ai-platform", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "standard-webhooks", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://platform.openai.com/docs/guides/webhooks", + "status": "complete", + "owner": null, + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "resend", + "label": "Resend", + "category": "email", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "svix", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://resend.com/docs/dashboard/webhooks/introduction", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "stripe", + "label": "Stripe", + "category": "payments", + "tier": "first-class", + "preset": "stripe", + "signatureScheme": "stripe", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "hookdeck", + "docsUrl": "https://docs.stripe.com/webhooks", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "whatsapp", + "label": "WhatsApp (Meta)", + "category": "communication", + "tier": "first-class", + "preset": "github", + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks/", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "brex", + "label": "Brex", + "category": "fintech", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "svix", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://developer.brex.com/docs/webhooks/", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "recall-ai", + "label": "Recall.ai", + "category": "ai-platform", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "standard-webhooks", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://docs.recall.ai/docs/authenticating-requests-from-recallai", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "replicate", + "label": "Replicate", + "category": "ai-platform", + "tier": "first-class", + "preset": "svix", + "signatureScheme": "standard-webhooks", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://replicate.com/docs/topics/webhooks/verify-webhook", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "square", + "label": "Square", + "category": "payments", + "tier": "first-class", + "preset": "square", + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://developer.squareup.com/docs/webhooks/step3validate", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "auth0", + "label": "Auth0", + "category": "auth-identity", + "tier": "sample-only", + "preset": null, + "signatureScheme": "bearer", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://auth0.com/docs/customize/log-streams/custom-log-streams", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "calendly", + "label": "Calendly", + "category": "calendar-scheduling", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "docusign", + "label": "DocuSign", + "category": "e-signature", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developers.docusign.com/platform/webhooks/connect/", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "elevenlabs", + "label": "ElevenLabs", + "category": "voice", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "hubspot", + "label": "HubSpot", + "category": "crm", + "tier": "sample-only", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "hookdeck", + "docsUrl": "https://developers.hubspot.com/docs/guides/api/app-management/webhooks", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "hookdeck samples are CRM-object shape, not App-Subscription array; revisit eventTypeSource/handauthor later" + }, + { + "id": "intercom", + "label": "Intercom", + "category": "support", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha1", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developers.intercom.com/docs/references/webhooks/webhook-models", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "notion", + "label": "Notion", + "category": "productivity", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developers.notion.com/reference/webhooks", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "pagerduty", + "label": "PagerDuty", + "category": "observability", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "plaid", + "label": "Plaid", + "category": "fintech", + "tier": "sample-only", + "preset": null, + "signatureScheme": "jwt", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://plaid.com/docs/api/webhooks/webhook-verification/", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "sendgrid", + "label": "SendGrid", + "category": "email", + "tier": "sample-only", + "preset": null, + "signatureScheme": "ecdsa", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "sentry", + "label": "Sentry", + "category": "observability", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "shopify", + "label": "Shopify", + "category": "commerce", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "hookdeck", + "docsUrl": "https://shopify.dev/docs/apps/build/webhooks/verify-deliveries", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "slack", + "label": "Slack", + "category": "communication", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://docs.slack.dev/authentication/verifying-requests-from-slack/", + "status": "complete", + "owner": null, + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "supabase", + "label": "Supabase", + "category": "hosting-infra", + "tier": "sample-only", + "preset": null, + "signatureScheme": "shared-secret", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://supabase.com/docs/guides/database/webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "twilio", + "label": "Twilio", + "category": "communication", + "tier": "sample-only", + "preset": null, + "signatureScheme": "hmac-sha1", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://www.twilio.com/docs/usage/webhooks/webhooks-security", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "two samples share eventType call.status-callback; split in cleanup" + }, + { + "id": "typeform", + "label": "Typeform", + "category": "forms", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "vercel", + "label": "Vercel", + "category": "hosting-infra", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha1", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://vercel.com/docs/webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "zendesk", + "label": "Zendesk", + "category": "support", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developer.zendesk.com/documentation/webhooks/verifying/", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "zoom", + "label": "Zoom", + "category": "communication", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "high", + "sampleSource": "handauthored", + "docsUrl": "https://developers.zoom.us/docs/api/webhooks/", + "status": "complete", + "owner": "batch1", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "assemblyai", + "label": "AssemblyAI", + "category": "ai-platform", + "tier": "sample-only", + "preset": null, + "signatureScheme": "shared-secret", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://www.assemblyai.com/docs/concepts/webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "attio", + "label": "Attio", + "category": "crm", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://docs.attio.com/rest-api/guides/webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "cal-com", + "label": "Cal.com", + "category": "calendar-scheduling", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://cal.com/docs/developing/guides/automation/webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "close-crm", + "label": "Close", + "category": "crm", + "tier": "sample-only", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://developer.close.com/topics/webhooks/", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "deepgram", + "label": "Deepgram", + "category": "ai-platform", + "tier": "sample-only", + "preset": null, + "signatureScheme": "none-or-ip-allowlist", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://developers.deepgram.com/docs/callback", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "linear", + "label": "Linear", + "category": "pm", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://linear.app/developers/webhooks", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "postmark", + "label": "Postmark", + "category": "email", + "tier": "sample-only", + "preset": null, + "signatureScheme": "basic", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://postmarkapp.com/developer/webhooks/inbound-webhook", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "retell", + "label": "Retell AI", + "category": "voice", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://docs.retellai.com/features/secure-webhook", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "telegram", + "label": "Telegram", + "category": "communication", + "tier": "sample-only", + "preset": null, + "signatureScheme": "shared-secret", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://core.telegram.org/bots/api#setwebhook", + "status": "complete", + "owner": "batch2", + "checklist": { + "registryEntry": true, + "samples": true + }, + "notes": "" + }, + { + "id": "vapi", + "label": "Vapi", + "category": "voice", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://docs.vapi.ai/server-url/server-authentication", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + }, + { + "id": "workos", + "label": "WorkOS", + "category": "auth-identity", + "tier": "first-class", + "preset": null, + "signatureScheme": "hmac-sha256", + "aiRelevance": "high", + "popularity": "medium", + "sampleSource": "handauthored", + "docsUrl": "https://workos.com/docs/events/data-syncing/webhooks", + "status": "complete", + "owner": "batch3", + "checklist": { + "registryEntry": true, + "samples": true, + "roundTrip": true, + "producer": true + }, + "notes": "" + } + ] +} diff --git a/internal-packages/webhook-sources/catalog/research-notes-wave2.md b/internal-packages/webhook-sources/catalog/research-notes-wave2.md new file mode 100644 index 00000000000..79012cde13c --- /dev/null +++ b/internal-packages/webhook-sources/catalog/research-notes-wave2.md @@ -0,0 +1,114 @@ +# Webhook provider seed research, wave 2 (raw notes, 2026-07-14) + +Same format as research-notes.md. `preset` = exact match to one of ours (stripe/github/svix/square/ +discord) else null. Finalize tier/rank in the merge. + +## Support / helpdesk / live-chat + +- zendesk | Zendesk | support | hmac-sha256 base64 (X-Zendesk-Webhook-Signature over ts+body; X-Zendesk-Webhook-Signature-Timestamp; test secret documented) | preset=null | AI:high pop:high | https://developer.zendesk.com/documentation/webhooks/verifying/ +- intercom | Intercom | support | hmac-sha1 (X-Hub-Signature sha1= over raw body, key=client secret) | preset=null | AI:high pop:high | https://developers.intercom.com/docs/references/webhooks/webhook-models +- front | Front | support | hmac-sha256 base64 (X-Front-Signature over ts+":"+body; X-Front-Request-Timestamp) | preset=null | AI:med pop:med | https://dev.frontapp.com/docs/application-webhooks +- helpscout | Help Scout | support | hmac-sha1 base64 (X-HelpScout-Signature over raw body) | preset=null | AI:med pop:med | https://developer.helpscout.com/webhooks/ +- crisp | Crisp | support | hmac-sha256 (X-Crisp-Signature over "[ts;body]"; X-Crisp-Request-Timestamp) | preset=null | AI:med pop:med | https://docs.crisp.chat/references/web-hooks/v1/ +- freshdesk | Freshdesk | support | none (basic auth / custom header you configure) | preset=null | AI:high pop:high | https://support.freshdesk.com/support/solutions/articles/50000009511 +- freshchat | Freshchat | support | rsa-sha256 asymmetric (X-Freshchat-Signature, provider public key) | preset=null | AI:med pop:med | https://crmsupport.freshworks.com/support/solutions/articles/50000004461 +- gorgias | Gorgias | support | none (custom headers / OAuth2) | preset=null | AI:med pop:med (Shopify e-comm) | https://docs.gorgias.com/http-integrations-81822 +- livechat | LiveChat (Text Platform) | support | shared-secret in payload (secret_key field in body) | preset=null | AI:med pop:med | https://platform.text.com/docs/management/webhooks +- kustomer | Kustomer | support | none (custom headers) | preset=null | AI:med pop:med (enterprise) | https://developer.kustomer.com/kustomer-apps-platform/docs/outbound-webhooks +- chatwoot | Chatwoot | support | hmac-sha256 (X-Chatwoot-Signature sha256= over ts.body; caveat self-hosted secret bug) | preset=null | AI:med pop:med (self-host) | https://developers.chatwoot.com/api-reference/webhooks/add-a-webhook +- aircall | Aircall | voice/support | token-in-body (no hmac in official docs) | preset=null | AI:med pop:med | https://developer.aircall.io/tutorials/webhooks-guide/ +- EXCLUDE drift (product sunsetting 2026-03; docs 404) + +## Forms / e-signature / storage / CMS + +- typeform | Typeform | forms | hmac-sha256 base64 (Typeform-Signature sha256= over raw body) | preset=null | AI:high pop:high | https://www.typeform.com/developers/webhooks/secure-your-webhooks/ +- tally | Tally | forms | hmac-sha256 base64 (Tally-Signature; signingSecret optional) | preset=null | AI:med pop:med | https://tally.so/help/webhooks +- docusign | DocuSign (Connect) | e-signature | hmac-sha256 base64 (X-DocuSign-Signature-1..N; up to 100 keys for rotation) | preset=null | AI:high pop:high | https://developers.docusign.com/platform/webhooks/connect/validate/ +- dropbox-sign | Dropbox Sign (HelloSign) | e-signature | hmac-sha256 hex (event_hash over event_time+event_type, key=API key; not body; reply "Hello API Event Received") | preset=null | AI:med pop:med | https://developers.hellosign.com/docs/events/walkthrough/ +- box | Box | storage | hmac-sha256 base64 (BOX-SIGNATURE-PRIMARY/SECONDARY over body+BOX-DELIVERY-TIMESTAMP; 10min replay) | preset=null | AI:med pop:med | https://developer.box.com/guides/webhooks/v2/signatures-v2 +- dropbox | Dropbox (storage) | storage | hmac-sha256 hex (X-Dropbox-Signature over raw body, app secret; GET ?challenge= verify; change-ping only) | preset=null | AI:med pop:high | https://www.dropbox.com/developers/reference/webhooks +- google-drive | Google Drive (push/watch) | storage | none (X-Goog-Channel-Token equality; empty-body pings, re-fetch; channels expire) | preset=null | AI:high pop:high | https://developers.google.com/workspace/drive/api/guides/push +- segment | Segment | data | hmac-sha1 hex (X-Signature over raw body; optional; batching signs only first event) | preset=null | AI:med pop:high | https://segment.com/docs/connections/destinations/catalog/actions-webhook/ +- contentful | Contentful | cms | hmac-sha256 hex (x-contentful-signature over CANONICAL request string; x-contentful-signed-headers; x-contentful-timestamp) | preset=null | AI:med pop:med | https://www.contentful.com/developers/docs/webhooks/request-verification/ +- sanity | Sanity | cms | hmac-sha256 (sanity-webhook-signature t=,v1= over ts.body; @sanity/webhook pkg) | preset=null | AI:med pop:med | https://www.sanity.io/docs/compute-and-ai/webhooks +- webflow | Webflow | cms | hmac-sha256 hex (x-webflow-signature over "{x-webflow-timestamp}:{body}"; 5min replay) | preset=null | AI:med pop:med | https://developers.webflow.com/data/reference/request-signatures + +## Devtools / source control / CI / hosting / infra / observability + +- github | GitHub | source-control | hmac-sha256 hex (X-Hub-Signature-256 sha256=; legacy sha1 too) | preset=github | AI:high pop:high | https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries | (in 19) +- gitlab | GitLab | source-control | default X-Gitlab-Token plaintext equality; newer webhook-signature v1, HMAC-SHA256 over message_id.ts.body (standard-webhooks) | preset=null (svix-family for signing token) | AI:high pop:high | https://docs.gitlab.com/user/project/integrations/webhooks/ | (in 19) +- bitbucket | Bitbucket | source-control | hmac-sha256 (X-Hub-Signature sha256=, recent; older unsigned) | preset=null (github-like) | AI:med pop:med | https://support.atlassian.com/bitbucket-cloud/docs/manage-webhooks/ | (in 19) +- circleci | CircleCI | ci-cd | hmac-sha256 hex (circleci-signature v1=) | preset=null | AI:med pop:med | https://circleci.com/docs/guides/integration/outbound-webhooks/ +- buildkite | Buildkite | ci-cd | hmac-sha256 (X-Buildkite-Signature timestamp=,signature= over ts.body; or X-Buildkite-Token plaintext) | preset=null | AI:med pop:med | https://buildkite.com/docs/apis/webhooks/pipelines +- vercel | Vercel | hosting-infra | hmac-sha1 hex (x-vercel-signature over raw body; account webhooks Pro/Ent) | preset=null | AI:med pop:high | https://vercel.com/docs/webhooks +- netlify | Netlify | hosting-infra | jws/jwt HS256 (X-Webhook-Signature JWT carries sha256 body-hash claim) | preset=null | AI:med pop:med | https://docs.netlify.com/site-deploys/notifications/ +- cloudflare | Cloudflare (Notifications) | hosting-infra | shared-secret (cf-webhook-auth plaintext; no body sig) | preset=null | AI:med pop:high | https://developers.cloudflare.com/notifications/get-started/configure-webhooks/ +- render | Render | hosting-infra | svix/standard-webhooks (webhook-id/timestamp/signature over id.ts.body) | preset=svix | AI:med pop:med | https://render.com/docs/webhooks +- aws-sns | AWS SNS (HTTP subscription) | hosting-infra | rsa asymmetric (Signature+SigningCertURL in body; v2=SHA256; SubscriptionConfirmation handshake) | preset=null | AI:med pop:high | https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html +- snyk | Snyk | observability/security | hmac-sha256 hex (X-Hub-Signature sha256=) | preset=null (github-like) | AI:med pop:med | https://docs.snyk.io/snyk-api/using-specific-snyk-apis/webhooks-apis/about-webhooks +- EXCLUDE fly-io (not self-serve; extension/partner only) unless we want it flagged + +## CRM / PM / productivity + +- linear | Linear | productivity/pm | hmac-sha256 hex (Linear-Signature over raw body; webhookTimestamp in body ~1min replay; Linear-Delivery) | preset=null | AI:high pop:high | https://linear.app/developers/webhooks +- jira | Jira (Atlassian Cloud) | pm | classic X-Hub-Signature HMAC (secret optional); dynamic webhooks JWT/bearer | preset=null (github-like for classic) | AI:med pop:high | https://developer.atlassian.com/cloud/jira/platform/webhooks/ +- notion | Notion | productivity | hmac-sha256 (X-Notion-Signature over raw body, verification_token from one-time setup; SDK verifyWebhookSignature) | preset=null | AI:high pop:high | https://developers.notion.com/reference/webhooks +- airtable | Airtable | productivity | hmac-sha256 (X-Airtable-Content-MAC hmac-sha256=; PING/POLL, then fetch payloads) | preset=null | AI:high pop:high | https://airtable.com/developers/web/api/webhooks-overview +- hubspot | HubSpot | crm | v3 HMAC-SHA256 base64 (X-HubSpot-Signature-v3 over method+uri+body+ts; v1/v2 are plain sha256(secret+body)) | preset=null | AI:high pop:high | https://developers.hubspot.com/docs/guides/api/app-management/webhooks | (in 19) +- salesforce | Salesforce | crm | none (Outbound Messages SOAP unsigned; or Event Relay to EventBridge) | preset=null | AI:high pop:high | https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_om_outboundmessaging_understanding.htm | CAVEAT: SOAP/unsigned or EventBridge-only, odd fit +- pipedrive | Pipedrive | crm | none (HTTP Basic Auth http_auth_user/password) | preset=null | AI:med pop:med | https://developers.pipedrive.com/docs/api/v1/Webhooks | (in 19) +- attio | Attio | crm | hmac-sha256 hex (Attio-Signature over raw body) | preset=null | AI:high pop:med | https://docs.attio.com/rest-api/guides/webhooks +- asana | Asana | pm | hmac-sha256 (X-Hook-Signature; X-Hook-Secret handshake echo; compact payloads, then fetch) | preset=null | AI:med pop:high | https://developers.asana.com/docs/webhooks-guide +- trello | Trello | pm | hmac-sha1 base64 (X-Trello-Webhook over body+callbackURL, key=OAuth secret) | preset=null | AI:med pop:high | https://developer.atlassian.com/cloud/trello/guides/rest-api/webhooks/ +- pandadoc | PandaDoc | e-signature | hmac-sha256 hex (signature in ?signature= QUERY PARAM, not header) | preset=null | AI:med pop:med | https://developers.pandadoc.com/docs/webhook-verification + +## Billing / subscriptions / commerce + +- paddlebilling | Paddle Billing | billing | hmac-sha256 hex (Paddle-Signature ts=,h1= over ts:body) | preset=null | AI:high pop:high | https://developer.paddle.com/webhooks/signature-verification | (in 19) +- paddleclassic | Paddle Classic | billing | rsa public-key (p_signature in form body, DKIM-style) | preset=null | AI:low pop:med (legacy) | https://developer.paddle.com/classic/reference/ZG9jOjI1MzUzOTg2-verifying-webhooks | (in 19) +- lemonsqueezy | Lemon Squeezy | billing | hmac-sha256 hex (X-Signature over raw body) | preset=null | AI:med pop:high | https://docs.lemonsqueezy.com/help/webhooks/signing-requests +- chargebee | Chargebee | billing | none (basic auth / URL secret) | preset=null | AI:med pop:high | https://www.chargebee.com/docs/2.0/webhook_settings.html +- recurly | Recurly | billing | hmac-sha256 (recurly-signature ts,sig; older XML unsigned) | preset=null | AI:med pop:med | https://recurly.com/developers/reference/webhooks/ +- revenuecat | RevenueCat | billing | static Authorization header default; optional hmac-sha256 (X-RevenueCat-Webhook-Signature t=,v1= over ts.body) | preset=null | AI:high pop:high (mobile subs) | https://www.revenuecat.com/docs/integrations/webhooks +- shopify | Shopify | commerce | hmac-sha256 base64 (X-Shopify-Hmac-Sha256 over raw body, key=app secret) | preset=null | AI:high pop:high | https://shopify.dev/docs/apps/build/webhooks/verify-deliveries | (in 19) +- bigcommerce | BigCommerce | commerce | none (custom headers you set; payload hash field; no official HMAC) | preset=null | AI:med pop:med | https://developer.bigcommerce.com/docs/integrations/webhooks/https | (in 19) +- woocommerce | WooCommerce | commerce | hmac-sha256 base64 (X-WC-Webhook-Signature over raw body) | preset=null | AI:med pop:high | https://woocommerce.github.io/woocommerce-rest-api-docs/#webhooks | (in 19) +- wix | Wix | commerce | jwt rsa (event data delivered as signed JWT; verify w/ app public key) | preset=null | AI:med pop:med | https://dev.wix.com/docs/build-apps/develop-your-app/access/authentication/verify-requests-received-from-wix +- commercelayer | Commerce Layer | commerce | hmac-sha256 (x-commercelayer-signature over raw body) | preset=null | AI:low pop:low-med | https://docs.commercelayer.io/core/callbacks-security | (in 19) +- fastspring | FastSpring | billing | hmac-sha256 base64 (X-FS-Signature; optional) | preset=null | AI:med pop:med | https://developer.fastspring.com/docs/message-security +- gumroad | Gumroad | commerce | none (form-urlencoded ping, seller_id check, URL secrecy) | preset=null | AI:low pop:med | https://gumroad.com/ping + +## Email / marketing + +- resend | Resend | email | svix (svix-id/timestamp/signature; SDK webhooks.verify) | preset=svix | AI:high pop:high | https://resend.com/docs/dashboard/webhooks/introduction +- sendgrid | SendGrid | email | ecdsa asymmetric (X-Twilio-Email-Event-Webhook-Signature + -Timestamp; EC public key) | preset=null | AI:high pop:high | https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features +- postmark | Postmark | email | none (basic auth / IP allowlist; no HMAC) | preset=null | AI:high pop:high | https://postmarkapp.com/developer/webhooks/webhooks-overview +- mailgun | Mailgun | email | hmac-sha256 hex (signature object IN BODY: timestamp+token; not header) | preset=null | AI:high pop:high | https://documentation.mailgun.com/docs/mailgun/user-manual/webhooks/securing-webhooks +- loops | Loops | email | hmac-sha256 (Webhook-Id/Timestamp/Signature over id.ts.body = standard-webhooks; svix-compatible) | preset=svix? (standard-webhooks) | AI:med pop:med | https://loops.so/docs/webhooks +- customerio | Customer.io | email/marketing | hmac-sha256 hex (X-CIO-Signature over "v0:{ts}:{body}"; X-CIO-Timestamp) | preset=null | AI:high pop:high | https://docs.customer.io/integrations/api/webhooks/ +- mailchimp | Mailchimp (Marketing) | marketing | none (URL secret only; x-www-form-urlencoded) | preset=null | AI:med pop:high | https://mailchimp.com/developer/marketing/guides/sync-audience-data-webhooks/ +- mandrill | Mandrill (MC Transactional) | email | hmac-sha1 base64 (X-Mandrill-Signature over URL+sorted params) | preset=null | AI:med pop:med | https://mailchimp.com/developer/transactional/guides/track-respond-activity-webhooks/ +- klaviyo | Klaviyo | marketing | hmac-sha256 (Klaviyo-Signature + Klaviyo-Timestamp; batched) | preset=null | AI:high pop:high (ecomm) | https://developers.klaviyo.com/en/docs/working_with_system_webhooks +- braze | Braze (Currents Custom HTTP) | marketing | none (optional bearer token; Currents-gated) | preset=null | AI:med pop:med | https://www.braze.com/docs/user_guide/data/distribution/braze_currents/setting_up_currents/custom_http_connector + +## Fintech / banking / crypto + +- plaid | Plaid | fintech | jwt es256 asymmetric (Plaid-Verification JWT; JWK via /webhook_verification_key/get; body sha256 claim; 5min) | preset=null | AI:high pop:high | https://plaid.com/docs/api/webhooks/webhook-verification/ +- coinbase-commerce | Coinbase Commerce | fintech/crypto | hmac-sha256 (X-CC-Webhook-Signature over raw body) | preset=null | AI:med pop:med | https://docs.cdp.coinbase.com/commerce/api-arcitecture/webhooks-security +- gocardless | GoCardless | fintech | hmac-sha256 (Webhook-Signature over raw body) | preset=null | AI:med pop:med | https://developer.gocardless.com/getting-started/stay-up-to-date-with-webhooks-v2/ +- dwolla | Dwolla | fintech | hmac-sha256 (X-Request-Signature-SHA-256 over raw body) | preset=null | AI:med pop:med | https://developers.dwolla.com/docs/balance/webhooks/process-validate +- ramp | Ramp | fintech | hmac-sha256 (X-Ramp-Signature over raw body) | preset=null | AI:high pop:high | https://docs.ramp.com/developer-api/v1/webhooks +- brex | Brex | fintech | svix (svix-id/timestamp/signature, whsec_) | preset=svix | AI:med pop:med-high | https://developer.brex.com/docs/webhooks/ +- mercury | Mercury | fintech | svix-style (whsec_, hmac-sha256; header name unconfirmed) | preset=svix? | AI:med pop:med-high | https://docs.mercury.com/reference/webhooks +- mollie | Mollie | fintech | classic=none (re-fetch); next-gen X-Mollie-Signature sha256= hmac-sha256 | preset=null | AI:med pop:high | https://docs.mollie.com/reference/webhooks-new +- razorpay | Razorpay | fintech | hmac-sha256 hex (X-Razorpay-Signature; x-razorpay-event-id for dedup) | preset=null | AI:high pop:high | https://razorpay.com/docs/webhooks/validate-test/ +- wise | Wise | fintech | rsa asymmetric (X-Signature-SHA256 base64; Wise public key) | preset=null | AI:med pop:med-high | https://docs.wise.com/api-docs/features/webhooks-notifications/event-handling +- bitpay | BitPay | fintech/crypto | none by default (re-fetch invoice via API) | preset=null | AI:low pop:med | https://developer.bitpay.com/docs/invoice-webhooks +- modern-treasury | Modern Treasury | fintech | hmac-sha256 hex (X-Signature over raw body) | preset=null | AI:med pop:med | https://docs.moderntreasury.com/docs/verifying-webhooks + +## Wave 2 preset/cluster notes + +- svix preset cluster grows: resend, brex, mercury (+ replicate/openai/anthropic/clerk from wave 1). standard-webhooks-compatible (same wire format, verify as svix): loops, render, gitlab-signing-token. +- github preset (X-Hub-Signature-256 sha256= HMAC over raw body): github, whatsapp/meta. Close cousins on X-Hub-Signature (no -256): bitbucket (sha256), snyk (sha256), intercom (sha1), jira-classic. Worth a "github/x-hub" verifier family. +- Distinct schemes needing custom configs: ecdsa (sendgrid), es256-jwt (plaid), rsa (paypal, wise, aws-sns, freshchat, paddle-classic, wix), in-body hmac (adyen, mailgun), canonical-request (contentful), url+params hmac (mandrill, trello), query-param sig (pandadoc), ping/poll (airtable, dropbox, google-drive, asana). +- No-signature (sample-only, ship as picker samples but no round-trip): freshdesk, gorgias, livechat, kustomer, aircall, google-drive, mailchimp-marketing, postmark, chargebee, bigcommerce, gumroad, pipedrive, salesforce, cloudflare, revenuecat-default, bitpay, mollie-classic, braze. diff --git a/internal-packages/webhook-sources/catalog/research-notes-wave3.md b/internal-packages/webhook-sources/catalog/research-notes-wave3.md new file mode 100644 index 00000000000..03c7a2d4906 --- /dev/null +++ b/internal-packages/webhook-sources/catalog/research-notes-wave3.md @@ -0,0 +1,35 @@ +# Webhook provider seed research, wave 3 (auth / observability / AI-infra additions, 2026-07-14) + +New providers not already in research-notes.md or wave2. + +## Observability + +- sentry | Sentry | observability | hmac-sha256 hex (sentry-hook-signature) | preset=null | AI:high pop:high | https://docs.sentry.io/product/integrations/integration-platform/webhooks/ +- datadog | Datadog | observability | none (custom static header only, e.g. API key) | preset=null | AI:high pop:high | https://docs.datadoghq.com/integrations/webhooks/ +- pagerduty | PagerDuty | observability | hmac-sha256 hex (x-pagerduty-signature v1=; v3 webhooks) | preset=null | AI:high pop:high | https://developer.pagerduty.com/docs/webhooks/webhook-signatures/ + +## Auth / identity + +- auth0 | Auth0 | auth-identity | bearer-token (Custom Webhook log streams; static Authorization) | preset=null | AI:high pop:high | https://auth0.com/docs/customize/log-streams/custom-log-streams +- workos | WorkOS | auth-identity | hmac-sha256 (WorkOS-Signature t=,v= comma-delimited; svix-like construction) | preset=null | AI:high pop:med | https://workos.com/docs/events/data-syncing/webhooks +- okta | Okta | auth-identity | shared-secret header + one-time GET challenge (no HMAC) | preset=null | AI:high pop:high | https://developer.okta.com/docs/concepts/event-hooks/ +- supabase | Supabase | hosting-infra/data | custom header shared secret (pg_net db webhooks; no built-in sig) | preset=null | AI:high pop:high | https://supabase.com/docs/guides/database/webhooks +- EXCLUDE firebase (Firestore/Auth triggers only invoke your own Cloud Functions; no outbound webhook) + +## AI infra / meeting + +- recall-ai | Recall.ai | ai-platform | svix (webhook-id/timestamp/signature, whsec_) | preset=svix | AI:high pop:med | https://docs.recall.ai/docs/authenticating-requests-from-recallai +- fireflies | Fireflies.ai | ai-platform | none/unknown | preset=null | AI:med pop:med | https://docs.fireflies.ai/graphql-api/webhooks + +## CRM / chat additions + +- close-crm | Close | crm | hmac-sha256 (close-sig-hash over ts+payload) | preset=null | AI:high pop:med | https://developer.close.com/topics/webhooks/ +- telegram | Telegram Bot API | communication | shared-secret header (X-Telegram-Bot-Api-Secret-Token, string compare) | preset=null | AI:high pop:high | https://core.telegram.org/bots/api#setwebhook + +## Forms addition + +- jotform | Jotform | forms | none (URL secret / re-fetch by submissionID) | preset=null | AI:med pop:med | https://www.jotform.com/help/245-how-to-send-submission-data-via-a-webhook/ + +## svix cluster now (round-trippable against our svix preset today) + +clerk, resend, openai, anthropic, replicate, brex, mercury, render, recall-ai (+ standard-webhooks-compatible: gitlab-signing-token, loops). diff --git a/internal-packages/webhook-sources/catalog/research-notes.md b/internal-packages/webhook-sources/catalog/research-notes.md new file mode 100644 index 00000000000..6c057885aa7 --- /dev/null +++ b/internal-packages/webhook-sources/catalog/research-notes.md @@ -0,0 +1,84 @@ +# Webhook provider seed research (raw notes, 2026-07-14) + +Findings from the seed-research subagents. `preset` = one of our exact presets (stripe/github/svix/ +square/discord) where the scheme matches exactly, else null (needs a custom config or ships sample-only). +Rank/tier finalized during the merge into providers.json. + +## Voice AI / conversational voice + +- elevenlabs | ElevenLabs | voice | hmac-sha256 (ElevenLabs-Signature, t=,v0=, over raw bytes; SDK constructEvent) | preset=null | AI:high pop:high | https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks +- vapi | Vapi | voice | hmac-sha256 (X-Vapi-Signature when secret set; else X-Vapi-Secret static) | preset=null | AI:high pop:high | https://docs.vapi.ai/server-url/server-authentication +- retell | Retell AI | voice | hmac-sha256 (x-retell-signature v=,d=; HMAC(body+ts, api_key); 5min replay; SDK verify) | preset=null | AI:high pop:high | https://docs.retellai.com/features/secure-webhook +- bland | Bland AI | voice | hmac-sha256 (X-Webhook-Signature; per webhook-signing doc) | preset=null | AI:med pop:med | https://docs.bland.ai/tutorials/webhook-signing + +## AI / ML compute / STT + +- replicate | Replicate | ai-platform | svix (standard-webhooks exactly, whsec_) | preset=svix | AI:high pop:high | https://replicate.com/docs/topics/webhooks/verify-webhook +- openai | OpenAI | ai-platform | svix (standard-webhooks, whsec_, SDK unwrap) | preset=svix | AI:high pop:high | https://developers.openai.com/api/docs/guides/webhooks +- anthropic | Anthropic (Claude Managed Agents) | ai-platform | svix (X-Webhook-Signature, whsec_, unwrap; NOT for Message Batches) | preset=svix | AI:high pop:high | https://platform.claude.com/docs/en/managed-agents/webhooks +- assemblyai | AssemblyAI | ai-platform | shared-secret (caller-defined webhook_auth_header_name/value, echoed; NOT hmac) | preset=null | AI:med pop:med | https://www.assemblyai.com/docs/concepts/webhooks +- deepgram | Deepgram | ai-platform | none/callback (per-request ?callback=; dg-token or basic auth; no hmac) | preset=null | AI:med pop:med | https://developers.deepgram.com/docs/callback +- huggingface | Hugging Face | ai-platform | basic/shared-secret (X-Webhook-Secret static, or ?secret=) | preset=null | AI:med pop:med | https://huggingface.co/docs/hub/en/webhooks +- runpod | RunPod | ai-platform | none (per-request webhook field; no signing) | preset=null | AI:med pop:med | https://docs.runpod.io/serverless/endpoints/send-requests +- EXCLUDE together-ai (Batch API poll-only, no webhook) +- EXCLUDE modal (inbound web functions only, no outbound webhook) + +## Communication / video + +- slack | Slack | communication | hmac-sha256 (v0:{ts}:{body}, X-Slack-Signature v0=, X-Slack-Request-Timestamp, 5min replay) | preset=null | AI:high pop:high | https://docs.slack.dev/authentication/verifying-requests-from-slack/ +- discord | Discord | communication | ed25519 (X-Signature-Ed25519 + X-Signature-Timestamp over ts+body; both Webhook Events + Interactions) | preset=discord | AI:high pop:high | https://docs.discord.com/developers/events/webhook-events +- microsoft-teams | Microsoft Teams (Outgoing Webhooks) | communication | hmac-sha256 (over raw body, base64 token, Authorization header; NOT the Workflows connector) | preset=null | AI:high pop:high | https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-outgoing-webhook +- zoom | Zoom | communication | hmac-sha256 (v0:{x-zm-request-timestamp}:{body}, x-zm-signature v0=; URL-validation challenge on setup) | preset=null | AI:high pop:high | https://developers.zoom.us/docs/api/webhooks/ +- webex | Cisco Webex | communication | hmac-sha1 (X-Spark-Signature over raw body) | preset=null | AI:med pop:med | https://developer.webex.com/messaging/docs/api/guides/webhooks +- google-calendar | Google Calendar (push/watch) | calendar-scheduling | shared-secret (X-Goog-Channel-Token echo, no crypto sig; thin notification, re-fetch) | preset=null | AI:high pop:high | https://developers.google.com/workspace/calendar/api/guides/push +- microsoft-graph | Microsoft Graph / Outlook | communication | clientState shared-secret default + signed JWT (validationTokens) for rich notifications; setup handshake echoes validationToken | preset=null | AI:high pop:high | https://learn.microsoft.com/en-us/graph/change-notifications-delivery-webhooks +- daily | Daily.co | communication | hmac-sha256 ({X-Webhook-Timestamp}.{body}, base64 secret, X-Webhook-Signature) | preset=null | AI:med pop:med | https://docs.daily.co/reference/rest-api/webhooks +- livekit | LiveKit | communication | jwt (Authorization JWT signed w/ api secret, carries sha256 body hash; SDK WebhookReceiver) | preset=null | AI:med pop:med | https://docs.livekit.io/home/server/webhooks/ +- whereby | Whereby | communication | hmac-sha256 (Stripe-style Whereby-Signature t=,v1=; {ts}.{body}) | preset=null | AI:low pop:med | https://docs.whereby.com/meeting-content-and-quality/insights-suite-and-api/webhooks +- EXCLUDE google-meet (Workspace Events API delivers to Pub/Sub only, no HTTP webhook) + +## SMS / voice / CPaaS + +- twilio | Twilio | communication | hmac-sha1 (X-Twilio-Signature base64 over URL + sorted POST params, key=Auth Token) | preset=null | AI:high pop:high | https://www.twilio.com/docs/usage/webhooks/webhooks-security +- vonage | Vonage | communication | jwt HS256 (Authorization: Bearer, signature secret; optional payload_hash claim) | preset=null | AI:med pop:med | https://developer.vonage.com/en/messages/code-snippets/configure-webhooks +- telnyx | Telnyx | communication | ed25519 (telnyx-signature-ed25519 + telnyx-timestamp over ts|payload; asymmetric public key) | preset=null | AI:med pop:med | https://developers.telnyx.com/docs/voice/programmable-voice/voice-api-webhooks +- bird | Bird (MessageBird) | communication | jwt HS256 (MessageBird-Signature-JWT over URL + body sha256 + ts; legacy raw hmac-sha256) | preset=null | AI:med pop:med | https://docs.bird.com/api/notifications-api/api-reference/webhook-subscriptions/verifying-a-webhook-subscription +- whatsapp | WhatsApp / Meta (Graph) | communication | hmac-sha256 (X-Hub-Signature-256 sha256= over raw body, app secret; GET subscribe handshake) SAME SCHEME AS GITHUB | preset=github? (verify header/format) | AI:high pop:high | https://developers.facebook.com/docs/graph-api/webhooks/getting-started +- plivo | Plivo | communication | hmac-sha256 (X-Plivo-Signature-V3 over URL + sorted params + nonce, base64) | preset=null | AI:med pop:med | https://www.plivo.com/docs/voice/concepts/signature-validation +- sinch | Sinch | communication | hmac-sha256 (X-Sinch-Signature over body.nonce.timestamp) | preset=null | AI:med pop:med | https://developers.sinch.com/docs/conversation/callbacks +- bandwidth | Bandwidth | communication | basic-auth (challenge-response 401 then Basic creds; no payload sig) | preset=null | AI:med pop:med | https://dev.bandwidth.com/docs/numbers/webhooks/ +- ringcentral | RingCentral | communication | shared-secret (Validation-Token echo on setup + optional static Verification Token header; no per-payload sig) | preset=null | AI:med pop:med | https://developers.ringcentral.com/guide/notifications/webhooks/creating-webhooks + +## Payments / commerce / fintech + +- stripe | Stripe | payments | stripe (Stripe-Signature t=,v1=, HMAC-SHA256 {ts}.{body}) | preset=stripe | AI:high pop:high | https://docs.stripe.com/webhooks/signature | (already in 19) +- stripe-issuing | Stripe Issuing | payments | stripe (identical to core Stripe; real-time auth events) | preset=stripe | AI:med pop:med | https://docs.stripe.com/issuing/controls/real-time-authorizations +- paypal | PayPal | payments | rsa-sha256 asymmetric (PAYPAL-TRANSMISSION-SIG + cert URL; verify offline or via API) | preset=null | AI:high pop:high | https://developer.paypal.com/api/rest/webhooks/rest/ | (already in 19) +- square | Square | payments/commerce | hmac-sha256 (x-square-hmacsha256-signature over notificationURL + body; URL in signed string) | preset=square | AI:high pop:high | https://developer.squareup.com/docs/webhooks/step3validate +- adyen | Adyen | payments | hmac-sha256 (signature IN PAYLOAD additionalData.hmacSignature, not header) | preset=null | AI:high pop:high | https://docs.adyen.com/development-resources/webhooks/secure-webhooks/verify-hmac-signatures | (already in 19) +- braintree | Braintree | payments | sdk form-param (bt_signature + bt_payload, hmac-sha1 based; SDK parse only) | preset=null | AI:med pop:med | https://developer.paypal.com/braintree/docs/guides/webhooks/parse/node +- checkout-com | Checkout.com | payments | hmac-sha256 (Cko-Signature over raw body, hex) | preset=null | AI:med pop:med | https://www.checkout.com/docs/developer-resources/event-notifications/receive-webhooks | (maps to existing "checkout" samples) +- authorize-net | Authorize.Net | payments | hmac-sha512 (X-ANET-Signature sha512= over JSON body) | preset=null | AI:med pop:med | https://developer.authorize.net/api/reference/features/webhooks.html +- klarna | Klarna | payments/bnpl | hmac-sha256 (Klarna-Signature over raw body + Klarna-Signing-Key-Id) | preset=null | AI:med pop:high | https://docs.klarna.com/ | CAVEAT: acquirer/route-dependent +- affirm | Affirm | payments/bnpl | hmac-sha512 (X-Affirm-Signature) | preset=null | AI:low pop:med | https://docs.affirm.com/developers/v1.1-developer-reference/docs/about-webhooks | CAVEAT: Key/Enterprise only, not self-serve + +## Calendar / scheduling + +- cal-com | Cal.com | calendar-scheduling | hmac-sha256 (X-Cal-Signature-256 hex over raw body) | preset=null | AI:high pop:high | https://cal.com/docs/developing/guides/automation/webhooks +- calendly | Calendly | calendar-scheduling | hmac-sha256 (Calendly-Webhook-Signature t=,v1= Stripe-style {ts}.{body}) | preset=null | AI:high pop:high | https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures +- acuity | Acuity Scheduling | calendar-scheduling | hmac-sha256 BASE64 (X-Acuity-Signature, key=API key) | preset=null | AI:med pop:high | https://developers.acuityscheduling.com/docs/webhooks +- nylas | Nylas | calendar-scheduling | hmac-sha256 (X-Nylas-Signature hex over raw body; compressed = hmac over compressed bytes) | preset=null | AI:med pop:med | https://developer.nylas.com/docs/v3/webhooks/ +- savvycal | SavvyCal | calendar-scheduling | hmac-sha256 (X-Savvycal-Signature sha256=hex) | preset=null | AI:low pop:med | https://savvycal.com/docs/api/webhooks/ +- chili-piper | Chili Piper | calendar-scheduling | none documented (URL secrecy / custom headers) | preset=null | AI:med pop:med | https://help.chilipiper.com | CAVEAT: no verifiable signature +- EXCLUDE doodle (enterprise-gated, undocumented, Zapier-only path) +- EXCLUDE zoho-bookings / zoho-calendar (no native outbound webhooks; webhooks live in Zoho Flow/CRM) + +## Preset-mapping notes for the merge + +- svix preset: replicate, openai, anthropic (+ clerk, resend already planned). +- stripe preset: stripe, stripe-issuing. +- square preset: square. +- discord preset: discord. +- github preset: github; VERIFY whatsapp/meta (X-Hub-Signature-256 sha256= looks identical to GitHub) as a potential github-preset reuse. +- Everything else (generic hmac variants w/ own header+template, ed25519 non-discord, jwt, rsa, basic, shared-secret) = custom verifier config authored later, or ships sample-only for v1. +- Notable signature-scheme diversity to cover in the sample library: hmac-sha1 (twilio, webex, braintree-ish), hmac-sha512 (authorize-net, affirm), rsa-sha256 (paypal), ed25519 (discord, telnyx), jwt-in-authorization (livekit, vonage, bird), in-payload hmac (adyen), shared-secret/echo (google-calendar, ms-graph default, ringcentral, assemblyai, huggingface, bland-alt). diff --git a/internal-packages/webhook-sources/catalog/status.ts b/internal-packages/webhook-sources/catalog/status.ts new file mode 100644 index 00000000000..a0b62f2b467 --- /dev/null +++ b/internal-packages/webhook-sources/catalog/status.ts @@ -0,0 +1,114 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +type Tier = "first-class" | "sample-only"; +type Status = "not-started" | "in-progress" | "complete" | "blocked"; + +type Provider = { + id: string; + label: string; + tier: Tier; + preset: string | null; + sampleSource: string; + sampleCount: number; + status: Status; + owner: string | null; + checklist: Record; + notes: string; +}; + +type Catalog = { + release: string; + updatedAt: string | null; + definitionOfDone: Record; + statusValues: Status[]; + providers: Provider[]; +}; + +const catalogPath = fileURLToPath(new URL("./providers.json", import.meta.url)); +const catalog: Catalog = JSON.parse(readFileSync(catalogPath, "utf8")); + +const dodProgress = (p: Provider) => { + const keys = catalog.definitionOfDone[p.tier] ?? []; + const done = keys.filter((k) => p.checklist[k] === true).length; + return { done, total: keys.length, keys }; +}; + +const derivedComplete = (p: Provider) => { + const { done, total } = dodProgress(p); + return total > 0 && done === total; +}; + +const byStatus = (s: Status) => catalog.providers.filter((p) => p.status === s); + +const args = process.argv.slice(2); + +if (args.includes("--json")) { + const summary = { + release: catalog.release, + updatedAt: catalog.updatedAt, + total: catalog.providers.length, + counts: Object.fromEntries(catalog.statusValues.map((s) => [s, byStatus(s).length])), + remaining: catalog.providers.filter((p) => p.status !== "complete").length, + providers: catalog.providers.map((p) => ({ + id: p.id, + tier: p.tier, + status: p.status, + owner: p.owner, + progress: dodProgress(p), + derivedComplete: derivedComplete(p), + })), + }; + console.log(JSON.stringify(summary, null, 2)); + process.exit(0); +} + +const nextFlagIdx = args.indexOf("--next"); +if (nextFlagIdx !== -1) { + const n = Number(args[nextFlagIdx + 1] ?? "1") || 1; + const claimable = catalog.providers + .filter((p) => p.status === "not-started" && p.owner === null) + .slice(0, n) + .map((p) => p.id); + console.log(claimable.join("\n")); + process.exit(0); +} + +const line = (p: Provider) => { + const { done, total } = dodProgress(p); + const owner = p.owner ? ` owner=${p.owner}` : ""; + const drift = p.status === "complete" && !derivedComplete(p) ? " [!] checklist incomplete" : ""; + const samples = p.sampleCount > 0 ? `${p.sampleCount} samples` : "no samples yet"; + return ` ${p.id.padEnd(12)} [${p.tier.padEnd(11)}] ${done}/${total} preset=${ + p.preset ?? "none" + } ${samples}${owner}${drift}`; +}; + +const total = catalog.providers.length; +const counts = Object.fromEntries(catalog.statusValues.map((s) => [s, byStatus(s).length])); +const remaining = catalog.providers.filter((p) => p.status !== "complete").length; + +console.log(`\nWebhook provider catalog: ${catalog.release}`); +console.log(`Updated: ${catalog.updatedAt ?? "never"}`); +console.log( + `Total ${total} | complete ${counts.complete} in-progress ${counts["in-progress"]} not-started ${counts["not-started"]} blocked ${counts.blocked}` +); +console.log(`Remaining (not complete): ${remaining}\n`); + +for (const s of catalog.statusValues) { + const group = byStatus(s); + if (group.length === 0) continue; + console.log(` ${s} (${group.length}):`); + for (const p of group) console.log(line(p)); + console.log(""); +} + +const claimable = catalog.providers.filter((p) => p.status === "not-started" && p.owner === null); +if (claimable.length > 0) { + console.log(`Next claimable: ${claimable.map((p) => p.id).join(", ")}`); +} else if (remaining === 0) { + console.log("All providers complete."); +} else { + console.log("Nothing claimable (remaining work is in-progress or blocked)."); +} +console.log(""); diff --git a/internal-packages/webhook-sources/package.json b/internal-packages/webhook-sources/package.json new file mode 100644 index 00000000000..09c6ae243a4 --- /dev/null +++ b/internal-packages/webhook-sources/package.json @@ -0,0 +1,22 @@ +{ + "name": "@internal/webhook-sources", + "private": true, + "version": "0.0.1", + "main": "./src/index.ts", + "types": "./src/index.ts", + "type": "module", + "dependencies": { + "@trigger.dev/core": "workspace:*", + "zod": "3.25.76" + }, + "devDependencies": { + "@internal/webhook-engine": "workspace:*" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest --run", + "ingest": "tsx src/ingest.ts", + "catalog:status": "tsx catalog/status.ts", + "catalog:aggregate": "tsx catalog/build-aggregates.ts" + } +} diff --git a/internal-packages/webhook-sources/src/generated/hookdeck-samples.json b/internal-packages/webhook-sources/src/generated/hookdeck-samples.json new file mode 100644 index 00000000000..b6d7aa6016a --- /dev/null +++ b/internal-packages/webhook-sources/src/generated/hookdeck-samples.json @@ -0,0 +1,33048 @@ +[ + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "AUTHENTICATION", + "name": "AUTHENTICATION", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "hmacSignature": "fVt4uUxoilauNk+iAynyImnjEmhfL5TsMgEy8sr3Qts=" + }, + "amount": { + "currency": "EUR", + "value": 0 + }, + "eventCode": "AUTHENTICATION", + "eventDate": "2023-03-08T10:44:17+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "testMerchantRef1", + "pspReference": "IAIBZKUKOJ77VBVN", + "reason": "", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "AUTHORISATION", + "name": "AUTHORISATION", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "expiryDate": "12/2012", + "authCode": "1234", + "cardSummary": "7777", + "totalFraudScore": "10", + "hmacSignature": "5j+QNzQ+JjZUZ4ELfDyHRk073Qtbj3vsrYbbswhSjfY=", + "NAME2": "VALUE2", + "NAME1": "VALUE1", + "fraudCheck-6-ShopperIpUsage": "10" + }, + "amount": { + "currency": "EUR", + "value": 10100 + }, + "eventCode": "AUTHORISATION", + "eventDate": "2023-03-08T10:45:05+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "8313842560770001", + "operations": [ + "CANCEL", + "CAPTURE", + "REFUND" + ], + "paymentMethod": "visa", + "pspReference": "NGAO2BVQFXJ7XZDP", + "reason": "1234:7777:12/2012", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "AUTHORISATION_ADJUSTMENT", + "name": "AUTHORISATION_ADJUSTMENT", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "hmacSignature": "Rt0X39s+cega7fRFLw7/oz+ntEpvYnjIrM0f4wePr5A=" + }, + "amount": { + "currency": "EUR", + "value": 0 + }, + "eventCode": "AUTHORISATION_ADJUSTMENT", + "eventDate": "2023-03-08T10:45:54+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "testMerchantRef1", + "pspReference": "FXVJF2KDXLRKMP13", + "reason": "", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "CANCEL_AUTORESCUE", + "name": "CANCEL_AUTORESCUE", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "hmacSignature": "8qwU5DK3kyotQyuZ065ERccqUZZN6kRiToh0dujLHL8=" + }, + "amount": { + "currency": "EUR", + "value": 0 + }, + "eventCode": "CANCEL_AUTORESCUE", + "eventDate": "2023-03-08T10:50:42+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "testMerchantRef1", + "pspReference": "1GOILQMXPH5TWPMR", + "reason": "", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "CANCEL_OR_REFUND", + "name": "CANCEL_OR_REFUND", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "hmacSignature": "7nZNwR2fG0eGoUHk04GAmJ6AZYRMjVlkXXouO/CuLrM=" + }, + "amount": { + "currency": "EUR", + "value": 0 + }, + "eventCode": "CANCEL_OR_REFUND", + "eventDate": "2023-03-08T10:51:22+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "testMerchantRef1", + "pspReference": "SVHWQYDNR0FWXQA0", + "reason": "", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "CANCELLATION", + "name": "CANCELLATION", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "hmacSignature": "8AitW6J9BnuCrcGpkNfnF/Td44Lf/WYkTY48GPfMpdQ=" + }, + "amount": { + "currency": "EUR", + "value": 0 + }, + "eventCode": "CANCELLATION", + "eventDate": "2023-03-08T10:46:31+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "testMerchantRef1", + "pspReference": "HLAESRA2W4YFAN01", + "reason": "", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "adyen", + "providerLabel": "Adyen", + "eventType": "CAPTURE", + "name": "CAPTURE", + "body": { + "live": "false", + "notificationItems": [ + { + "NotificationRequestItem": { + "additionalData": { + "hmacSignature": "M7Qsb8BCxhNTuXNQabdsSvp875QK48Z6qMGz0eHp5Ik=" + }, + "amount": { + "currency": "EUR", + "value": 0 + }, + "eventCode": "CAPTURE", + "eventDate": "2023-03-08T10:52:06+01:00", + "merchantAccountCode": "OctovieECOM", + "merchantReference": "testMerchantRef1", + "pspReference": "QBPLK35VG3MN6HXE", + "reason": "", + "success": "true" + } + } + ] + }, + "extraHeaders": { + "is-test-event": "true" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:adyen/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ampersand", + "providerLabel": "Ampersand", + "eventType": "intercom", + "name": "intercom", + "body": { + "projectId": "ampersand-project-id", + "provider": "intercom", + "groupRef": "xyz-company", + "groupName": "XYZ Company", + "installationId": "installation-id", + "objectName": "contacts", + "result": [ + { + "fields": { + "id": "6642abc67d11ea8432bd6029", + "location": { + "city": "Dublin", + "continent_code": "EU", + "country": "Ireland", + "country_code": "IRL", + "region": "Leinster", + "type": "location" + }, + "name": "Example User", + "notes": { + "data": [ + { + "id": "129149253", + "type": "note", + "url": "/notes/129149253" + }, + { + "id": "129149227", + "type": "note", + "url": "/notes/129149227" + } + ], + "has_more": false, + "total_count": 2, + "type": "list", + "url": "/contacts/6642abc67d11ea8432bd6029/notes" + }, + "workspace_id": "xyz" + }, + "raw": { + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_last_seen_at": null, + "android_os_version": null, + "android_sdk_version": null, + "avatar": null, + "browser": null, + "browser_language": null, + "browser_version": null, + "companies": { + "data": [ + { + "id": "6642abc67d11ea8432bd602a", + "type": "company", + "url": "/companies/6642abc67d11ea8432bd602a" + } + ], + "has_more": false, + "total_count": 1, + "type": "list", + "url": "/contacts/6642abc67d11ea8432bd6029/companies" + }, + "created_at": 1715645382, + "custom_attributes": {}, + "email": "example.user@projectmap.com", + "external_id": "6ef30a34-914f-4f73-94b8-810758f516bc", + "has_hard_bounced": false, + "id": "6642abc67d11ea8432bd6029", + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_last_seen_at": null, + "ios_os_version": null, + "ios_sdk_version": null, + "language_override": null, + "last_contacted_at": null, + "last_email_clicked_at": null, + "last_email_opened_at": null, + "last_replied_at": null, + "last_seen_at": 1715645382, + "location": { + "city": "Dublin", + "continent_code": "EU", + "country": "Ireland", + "country_code": "IRL", + "region": "Leinster", + "type": "location" + }, + "marked_email_as_spam": false, + "name": "Example User", + "notes": { + "data": [ + { + "id": "129149253", + "type": "note", + "url": "/notes/129149253" + }, + { + "id": "129149227", + "type": "note", + "url": "/notes/129149227" + } + ], + "has_more": false, + "total_count": 2, + "type": "list", + "url": "/contacts/6642abc67d11ea8432bd6029/notes" + }, + "opted_in_subscription_types": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/6642abc67d11ea8432bd6029/subscriptions" + }, + "opted_out_subscription_types": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/6642abc67d11ea8432bd6029/subscriptions" + }, + "os": null, + "owner_id": null, + "phone": null, + "referrer": null, + "role": "user", + "signed_up_at": 1715645382, + "sms_consent": false, + "social_profiles": { + "data": [], + "type": "list" + }, + "tags": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/6642abc67d11ea8432bd6029/tags" + }, + "type": "contact", + "unsubscribed_from_emails": false, + "unsubscribed_from_sms": false, + "updated_at": 1715645383, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "workspace_id": "xyz" + } + }, + { + "fields": { + "id": "664d2e644e1281ed9090338b", + "location": { + "city": null, + "continent_code": null, + "country": null, + "country_code": null, + "region": null, + "type": "location" + }, + "name": "Example Lead", + "notes": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/664d2e644e1281ed9090338b/notes" + }, + "workspace_id": "xyz" + }, + "raw": { + "android_app_name": null, + "android_app_version": null, + "android_device": null, + "android_last_seen_at": null, + "android_os_version": null, + "android_sdk_version": null, + "avatar": null, + "browser": null, + "browser_language": null, + "browser_version": null, + "companies": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/664d2e644e1281ed9090338b/companies" + }, + "created_at": 1716334180, + "custom_attributes": {}, + "email": "lead@example.com", + "external_id": "3d71a921-9276-407d-b3c1-3b506791a48d", + "has_hard_bounced": false, + "id": "664d2e644e1281ed9090338b", + "ios_app_name": null, + "ios_app_version": null, + "ios_device": null, + "ios_last_seen_at": null, + "ios_os_version": null, + "ios_sdk_version": null, + "language_override": null, + "last_contacted_at": null, + "last_email_clicked_at": null, + "last_email_opened_at": null, + "last_replied_at": null, + "last_seen_at": null, + "location": { + "city": null, + "continent_code": null, + "country": null, + "country_code": null, + "region": null, + "type": "location" + }, + "marked_email_as_spam": false, + "name": "Example Lead", + "notes": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/664d2e644e1281ed9090338b/notes" + }, + "opted_in_subscription_types": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/664d2e644e1281ed9090338b/subscriptions" + }, + "opted_out_subscription_types": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/664d2e644e1281ed9090338b/subscriptions" + }, + "os": null, + "owner_id": null, + "phone": null, + "referrer": null, + "role": "lead", + "signed_up_at": null, + "sms_consent": false, + "social_profiles": { + "data": [], + "type": "list" + }, + "tags": { + "data": [], + "has_more": false, + "total_count": 0, + "type": "list", + "url": "/contacts/664d2e644e1281ed9090338b/tags" + }, + "type": "contact", + "unsubscribed_from_emails": false, + "unsubscribed_from_sms": false, + "updated_at": 1716334180, + "utm_campaign": null, + "utm_content": null, + "utm_medium": null, + "utm_source": null, + "utm_term": null, + "workspace_id": "xyz" + } + } + ] + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ampersand/v1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ampersand", + "providerLabel": "Ampersand", + "eventType": "salesforce", + "name": "salesforce", + "body": { + "projectId": "ampersand-project-id", + "provider": "salesforce", + "workspace": "salesforce-subdomain", + "groupRef": "xyz-company", + "groupName": "XYZ Company", + "installationId": "installation-id", + "objectName": "contact", + "result": [ + { + "fields": { + "id": "001Dp00000P8QurIAF", + "firstname": "Sally", + "lastname": "Jones", + "email": "sally@jones.net" + }, + "mappedFields": { + "pronoun": "she" + }, + "raw": { + "id": "001Dp00000P8QurIAF", + "firstname": "Sally", + "lastname": "Jones", + "email": "sally@jones.net", + "pronoun_custom_field": "she" + } + }, + { + "fields": { + "id": "001Dp00000P9BusEIS", + "firstname": "Taylor", + "lastname": "Lao", + "email": "taylor.lao@company.com" + }, + "mappedFields": { + "pronoun": "they" + }, + "raw": { + "id": "001Dp00000P9BusEIS", + "firstname": "Taylor", + "lastname": "Lao", + "email": "taylor.lao@company.com", + "pronoun_custom_field": "they" + } + } + ] + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ampersand/v1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/brand/metafield/created", + "name": "store/brand/metafield/created", + "body": { + "scope": "store/brand/metafield/created", + "store_id": "1025646", + "data": { + "metafield_id": 16, + "resource_id": "38" + }, + "hash": "352e4afc6dd3fc85ea26bfdf3f91852604d57528", + "created_at": 1561482670, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/cart/converted", + "name": "store/cart/converted", + "body": { + "scope": "store/cart/converted", + "store_id": "1025646", + "data": { + "type": "cart", + "id": "d30016e2-23c0-4a90-884f-2e92ac135476", + "orderId": 252 + }, + "hash": "b86db7c77d7ef8f90d6a8aefa56de32ccd776923", + "created_at": 1561486893, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/cart/couponApplied", + "name": "store/cart/couponApplied", + "body": { + "scope": "store/cart/couponApplied", + "store_id": "1025646", + "data": { + "type": "cart", + "id": "09346904-4175-44fd-be53-f7e598531b6c", + "couponId": 1 + }, + "hash": "4b7297d295141b660e8db5a0d99dfcdf459fe825", + "created_at": 1561482761, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/cart/created", + "name": "store/cart/created", + "body": { + "scope": "store/cart/created", + "store_id": "1025646", + "data": { + "type": "cart", + "id": "09346904-4175-44fd-be53-f7e598531b6c" + }, + "hash": "352e4afc6dd3fc85ea26bfdf3f91852604d57528", + "created_at": 1561482670, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/cart/lineItem/created", + "name": "store/cart/lineItem/created", + "body": { + "scope": "store/cart/lineItem/created", + "store_id": "1025646", + "data": { + "type": "cart_line_item", + "id": "743bfd94-d5dd-47c5-9c19-6eec32ca6119", + "cartId": "b0386708-fef3-45de-9d8b-fbe3031450a4" + }, + "hash": "399321a1bf1ac1331e12826fb89f264b4c8d21a6", + "created_at": 1561481786, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/cart/metafield/created", + "name": "store/cart/metafield/created", + "body": { + "scope": "store/cart/metafield/created", + "store_id": "1025646", + "data": { + "metafield_id": 20, + "resource_id": "29520ea6-2d39-4b39-836e-06ea14bf9f69" + }, + "hash": "352e4afc6dd3fc85ea26bfdf3f91852604d57528", + "created_at": 1561482670, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/category/created", + "name": "store/category/created", + "body": { + "scope": "store/category/created", + "store_id": "1025646", + "data": { + "type": "category", + "id": 42 + }, + "hash": "dc3a47c15425d2c895dba674f86fe71a8f3b6459", + "created_at": 1561480214, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/category/metafield/created", + "name": "store/category/metafield/created", + "body": { + "scope": "store/category/metafield/created", + "store_id": "1025646", + "data": { + "metafield_id": 17, + "resource_id": "24" + }, + "hash": "dc3a47c15425d2c895dba674f86fe71a8f3b6459", + "created_at": 1561480214, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/*/inventory/product/stock_changed", + "name": "store/channel/*/inventory/product/stock_changed", + "body": { + "producer": "stores/{store_hash}", + "hash": "996ea203c939d05ca667a515ef2f029976d6df80", + "created_at": 1682624640, + "store_id": "1234567890", + "scope": "store/channel/*/inventory/product/stock_changed", + "data": { + "variant_id": 127, + "product_id": 113, + "location_id": 2 + } + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/cart/created", + "name": "store/channel/1/cart/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/cart/created", + "data": { + "cart_id": "41696c19-486f-40a8-ae2a-389d5d24e0c9" + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/cart/lineItems/created", + "name": "store/channel/1/cart/lineItems/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/cart/lineItems/created", + "data": { + "cart_id": "41696c19-486f-40a8-ae2a-389d5d24e0c9", + "cart_item_id": "af133539-0d83-464d-870d-776e2672e8f4" + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/category/created", + "name": "store/channel/1/category/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/category/created", + "data": { + "category_id": 35, + "tree_id": 1 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/categoryTree/updated", + "name": "store/channel/1/categoryTree/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/categoryTree/updated", + "data": { + "tree_id": 1 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/notifications/abandonedCart/updated", + "name": "store/channel/1/notifications/abandonedCart/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/notifications/abandonedCart/updated", + "data": {}, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/order/updated", + "name": "store/channel/1/order/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/order/updated", + "data": { + "order_id": 127 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/page/created", + "name": "store/channel/1/page/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/page/created", + "data": { + "page_id": 11 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/product/assigned", + "name": "store/channel/1/product/assigned", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/product/assigned", + "data": { + "product_id": 127 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/script/created", + "name": "store/channel/1/script/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/script/created", + "data": { + "uuid": "0187cc6c-cebf-45f9-93b8-7dd0a2e09774" + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/settings/emailStatus/updated", + "name": "store/channel/1/settings/emailStatus/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/settings/emailStatus/updated", + "data": { + "template_kind": "product_review_email" + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/settings/route/updated", + "name": "store/channel/1/settings/route/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/settings/route/updated", + "data": { + "site_id": 1000 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/settings/searchContextFilters/updated", + "name": "store/channel/1/settings/searchContextFilters/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/settings/searchContextFilters/updated", + "data": { + "category_id": 29 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/settings/site/updated", + "name": "store/channel/1/settings/site/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/settings/site/updated", + "data": { + "site_id": 1001 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/socialMediaLinks/updated", + "name": "store/channel/1/socialMediaLinks/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/socialMediaLinks/updated", + "data": {}, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/1/theme/configuration/created", + "name": "store/channel/1/theme/configuration/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/1/theme/configuration/created", + "data": { + "theme_id": "e3d82ce0-9bae-0133-0de7-525400970412", + "variation_id": "f49489c0-8678-013a-2933-5227bc3d7181", + "version_id": "f4337c30-8678-013a-2933-5227bc3d7181", + "configuration_id": "2dc1c3f0-b2cf-013a-a341-2aac1278f99c" + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/created", + "name": "store/channel/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1335335335, + "scope": "store/channel/created", + "data": { + "type": "channel", + "id": 2 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/channel/metafield/created", + "name": "store/channel/metafield/created", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/channel/metafield/created", + "data": { + "metafield_id": 21, + "resource_id": "1" + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/customer/address/created", + "name": "store/customer/address/created", + "body": { + "scope": "store/customer/address/created", + "store_id": "1025646", + "data": { + "type": "customer", + "id": 60, + "address": { + "customer_id": 32 + } + }, + "hash": "416ca9c01779515de91824aa1cac9012ee691e7a", + "created_at": 1561481620, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/customer/channel/login/access/updated", + "name": "store/customer/channel/login/access/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/customer/channel/login/access/updated", + "data": { + "customer_id": 22, + "channel_ids": [ + 1 + ] + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/customer/created", + "name": "store/customer/created", + "body": { + "scope": "store/customer/created", + "store_id": "1025646", + "data": { + "type": "customer", + "id": 32 + }, + "hash": "8768ab15aa86c6d73c7e4c3efbaee072110ad1d2", + "created_at": 1561481571, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/information/updated", + "name": "store/information/updated", + "body": { + "scope": "store/information/updated", + "store_id": "1025646", + "data": { + "type": "store" + }, + "hash": "c553845e0a5e28dc8b0ea494458692a25586a294", + "created_at": 1535489273, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/inventory/location/created", + "name": "store/inventory/location/created", + "body": { + "producer": "stores/{store_hash}", + "hash": "f23133c2b4caa5a8763b678fed2e3b643348599d", + "created_at": 1682627509, + "store_id": "1234567890", + "scope": "store/inventory/location/created", + "data": { + "location_id": 2 + } + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/inventory/location/metafield/created", + "name": "store/inventory/location/metafield/created", + "body": { + "scope": "store/inventory/location/metafield/created", + "store_id": "1001197568", + "data": { + "metafield_id": 10, + "resource_id": "1" + }, + "hash": "e0c298b8097a6a2f39d17e593a9b360f5b2fef7d", + "created_at": 1683303055, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/metafield/created", + "name": "store/metafield/created", + "body": { + "scope": "store/metafield/created", + "store_id": "1001197568", + "data": { + "metafield_id": 12, + "resource_id": "118", + "resource_type": "product" + }, + "hash": "60f42bd247b52f0647cbd16b1205c84260287141", + "created_at": 1683305167, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/metafield/updated", + "name": "store/metafield/updated", + "body": { + "scope": "store/metafield/updated", + "store_id": "1001197568", + "data": { + "metafield_id": 10, + "resource_id": "1", + "resource_type": "location" + }, + "hash": "e0c298b8097a6a2f39d17e593a9b360f5b2fef7d", + "created_at": 1683303055, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/modifier/updated", + "name": "store/modifier/updated", + "body": { + "scope": "store/modifier/updated", + "store_id": "1025646", + "data": { + "type": "shared_modifier", + "id": 205, + "affected_product_ids": [ + 1, + 2 + ], + "context": { + "channel_id": 2, + "locale": "fr" + } + }, + "hash": "a833a57fadd56a32dc752fb6ca0841dc9602a495", + "created_at": 1561479233, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/option/updated", + "name": "store/option/updated", + "body": { + "scope": "store/option/updated", + "store_id": "1025646", + "data": { + "type": "local_option", + "id": 205, + "affected_product_ids": [ + 1, + 2 + ], + "context": { + "channel_id": 2, + "locale": "fr" + } + }, + "hash": "a833a57fadd56a32dc752fb6ca0841dc9602a495", + "created_at": 1561479233, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/order/created", + "name": "store/order/created", + "body": { + "scope": "store/order/created", + "store_id": "1025646", + "data": { + "type": "order", + "id": 250 + }, + "hash": "dd70c0976e06b67aaf671e73f49dcb79230ebf9d", + "created_at": 1561479335, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/order/message/created", + "name": "store/order/message/created", + "body": { + "scope": "store/order/message/created", + "store_id": "1025646", + "data": { + "type": "order", + "id": 250, + "message": { + "order_message_id": 3 + } + }, + "hash": "cb07cdbdda8b1965e812693d5988154807eeed02", + "created_at": 1561479923, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/order/metafield/created", + "name": "store/order/metafield/created", + "body": { + "scope": "store/order/metafield/created", + "store_id": "1001197568", + "data": { + "metafield_id": 14, + "resource_id": "270" + }, + "hash": "8fd18449531d7e531587782eb200950cbad8cffe", + "created_at": 1683315012, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/order/refund/created", + "name": "store/order/refund/created", + "body": { + "scope": "store/order/refund/created", + "store_id": "1025646", + "data": { + "type": "order", + "id": 250, + "refund": { + "refund_id": 3 + } + }, + "hash": "cb07cdbdda8b1965e812693d5988154807eeed02", + "created_at": 1561479923, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/order/statusUpdated", + "name": "store/order/statusUpdated", + "body": { + "scope": "store/order/statusUpdated", + "store_id": "1025646", + "data": { + "type": "order", + "id": 250, + "status": { + "previous_status_id": 0, + "new_status_id": 11 + } + }, + "hash": "7ee67cd1cf2ca60bc1aa9e5fe957d2de373be4ca", + "created_at": 1561479335, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/order/transaction/created", + "name": "store/order/transaction/created", + "body": { + "scope": "store/order/transaction/created", + "store_id": "1025646", + "data": { + "type": "transaction", + "order_id": 250, + "transaction_id": "176342342", + "transaction_status": "complete", + "transaction_type": "capture", + "result": { + "code": "captured", + "message": "Captured", + "type": "success" + }, + "platform_transaction_id": "964899682", + "provider_transaction_id": "CK65FBPKR472P4V5" + }, + "hash": "dd70c0976e06b67aaf671e73f49dcb79230ebf9d", + "created_at": 1561479335, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/priceList/assignment/updated", + "name": "store/priceList/assignment/updated", + "body": { + "store_id": "11111", + "producer": "stores/abcde", + "created_at": 1641641646, + "scope": "store/priceList/assignment/updated", + "data": { + "price_list_id": 2, + "channel_id": 1, + "customer_group_id": 3 + }, + "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/priceList/created", + "name": "store/priceList/created", + "body": { + "scope": "store/priceList/created", + "store_id": "1001197568", + "data": { + "price_list_id": 5 + }, + "hash": "e0c298b8097a6a2f39d17e593a9b360f5b2fef7d", + "created_at": 1709327895, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/priceList/record/created", + "name": "store/priceList/record/created", + "body": { + "scope": "store/priceList/record/created", + "store_id": "1001197568", + "data": { + "price_list_id": 1, + "variant_id": 179, + "currency": "USD" + }, + "hash": "0424a15f158bbfe7277c5f84f9c55a1d4a762e60", + "created_at": 1709327895, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/priceLists/deleted", + "name": "store/priceLists/deleted", + "body": { + "scope": "store/priceLists/deleted", + "store_id": "1001197568", + "data": { + "price_list_ids": [ + 5 + ] + }, + "hash": "e0c298b8097a6a2f39d17e593a9b360f5b2fef7d", + "created_at": 1709327895, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/product/inventory/updated", + "name": "store/product/inventory/updated", + "body": { + "scope": "store/product/inventory/updated", + "store_id": "1025646", + "data": { + "type": "product", + "id": 167, + "inventory": { + "product_id": 167, + "method": "absolute", + "value": 100000000 + } + }, + "hash": "cba9eef399fbd6d384489bca6cacad24794b1086", + "created_at": 1561478843, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/product/updated", + "name": "store/product/updated", + "body": { + "scope": "store/product/updated", + "store_id": "1025646", + "data": { + "type": "product", + "id": 205, + "properties": [ + "warranty", + "is_featured", + "custom_fields" + ], + "context": { + "channel_id": 2, + "locale": "fr" + } + }, + "hash": "a833a57fadd56a32dc752fb6ca0841dc9602a495", + "created_at": 1561479233, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/shipment/created", + "name": "store/shipment/created", + "body": { + "scope": "store/shipment/created", + "store_id": "1025646", + "data": { + "type": "shipment", + "id": 12, + "orderId": 251 + }, + "hash": "8b98021cb0faa7e3a58a0e4182d3696a4bdd24ab", + "created_at": 1561482857, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/sku/created", + "name": "store/sku/created", + "body": { + "scope": "store/sku/created", + "store_id": "1025646", + "data": { + "type": "sku", + "id": 461, + "sku": { + "product_id": 206, + "variant_id": 509 + } + }, + "hash": "7a0866943b1f46cfda31c3218931f5aab83a4c73", + "created_at": 1561480465, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/sku/inventory/updated", + "name": "store/sku/inventory/updated", + "body": { + "scope": "store/sku/inventory/updated", + "store_id": "1025646", + "data": { + "type": "sku", + "id": 461, + "inventory": { + "product_id": 206, + "method": "absolute", + "value": 5, + "variant_id": 509 + } + }, + "hash": "116ddb29d7bc1b2322cc1a4dc295221ee3637d4b", + "created_at": 1561480673, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bigcommerce", + "providerLabel": "BigCommerce", + "eventType": "store/subscriber/created", + "name": "store/subscriber/created", + "body": { + "scope": "store/subscriber/created", + "store_id": "1025646", + "data": { + "type": "subscriber", + "id": 5 + }, + "hash": "bdb6c9c2d17ca7036538e483db0bdd7debc4beb4", + "created_at": 1561482953, + "producer": "stores/{store_hash}" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bigcommerce/2025-04", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "pullrequest:approved", + "name": "pullrequest:approved", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "pullrequest": { + "comment_count": 1, + "task_count": 0, + "type": "pullrequest", + "id": 1, + "title": "New changes-multiple", + "description": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "rendered": { + "title": { + "type": "rendered", + "raw": "New changes-multiple", + "markup": "markdown", + "html": "

New changes-multiple

" + }, + "description": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "state": "OPEN", + "merge_commit": null, + "close_source_branch": false, + "closed_by": null, + "author": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "reason": "", + "created_on": "2023-03-08T06:10:12.744328+00:00", + "updated_on": "2023-03-08T06:12:10.654730+00:00", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "type": "commit", + "hash": "0ccb80619bd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "source": { + "branch": { + "name": "new-branch" + }, + "commit": { + "type": "commit", + "hash": "0845504f4acb", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acb" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acb" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "reviewers": [], + "participants": [ + { + "type": "participant", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "role": "PARTICIPANT", + "approved": true, + "state": "approved", + "participated_on": "2023-03-08T06:12:10.654730+00:00" + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/commits" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/approve" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/request-changes" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diffstat/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/activity" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/merge" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/decline" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/statuses" + } + }, + "summary": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "approval": { + "date": "2023-03-08T06:12:10.654730+00:00", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "f8b56f7a244cfb56", + "x-b3-traceid": "f8b56f7a244cfb56", + "x-event-key": "pullrequest:approved", + "x-event-time": "Wed, 08 Mar 2023 06:12:10 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "b2f93568-9ff7-4532-adc0-43212479959d" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "pullrequest:comment_created", + "name": "pullrequest:comment_created", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "pullrequest": { + "comment_count": 1, + "task_count": 0, + "type": "pullrequest", + "id": 1, + "title": "New branch", + "description": "* testfile.md created online with Bitbucket\n* anotherfile.md created online with Bitbucket\n* thirdfile created online with Bitbucket\n\n‌", + "rendered": { + "title": { + "type": "rendered", + "raw": "New branch", + "markup": "markdown", + "html": "

New branch

" + }, + "description": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\n* anotherfile.md created online with Bitbucket\n* thirdfile created online with Bitbucket\n\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "state": "OPEN", + "merge_commit": null, + "close_source_branch": false, + "closed_by": null, + "author": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "reason": "", + "created_on": "2023-03-08T06:10:12.744328+00:00", + "updated_on": "2023-03-08T06:11:02.138668+00:00", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "type": "commit", + "hash": "0ccb80619bd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "source": { + "branch": { + "name": "new-branch" + }, + "commit": { + "type": "commit", + "hash": "0845504f4acb", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acb" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acb" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "reviewers": [], + "participants": [ + { + "type": "participant", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "role": "PARTICIPANT", + "approved": false, + "state": null, + "participated_on": "2023-03-08T06:11:02.138668+00:00" + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/commits" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/approve" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/request-changes" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diffstat/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/activity" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/merge" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/decline" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/statuses" + } + }, + "summary": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\n* anotherfile.md created online with Bitbucket\n* thirdfile created online with Bitbucket\n\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "comment": { + "id": 375719071, + "created_on": "2023-03-08T06:11:02.138623+00:00", + "updated_on": "2023-03-08T06:11:02.138668+00:00", + "content": { + "type": "rendered", + "raw": "What change?", + "markup": "markdown", + "html": "

What change?

" + }, + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "deleted": false, + "type": "pullrequest_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments/375719071" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1/_/diff#comment-375719071" + } + }, + "pullrequest": { + "type": "pullrequest", + "id": 1, + "title": "New branch", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + } + } + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "337c5f20ea645e9b", + "x-b3-traceid": "337c5f20ea645e9b", + "x-event-key": "pullrequest:comment_created", + "x-event-time": "Wed, 08 Mar 2023 06:11:02 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "42148ed7-5e52-4ffc-be60-16fadfd9d840" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "pullrequest:created", + "name": "pullrequest:created", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "pullrequest": { + "comment_count": 0, + "task_count": 0, + "type": "pullrequest", + "id": 1, + "title": "New branch", + "description": "* testfile.md created online with Bitbucket\n* anotherfile.md created online with Bitbucket\n* thirdfile created online with Bitbucket\n\n‌", + "rendered": { + "title": { + "type": "rendered", + "raw": "New branch", + "markup": "markdown", + "html": "

New branch

" + }, + "description": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\n* anotherfile.md created online with Bitbucket\n* thirdfile created online with Bitbucket\n\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "state": "OPEN", + "merge_commit": null, + "close_source_branch": false, + "closed_by": null, + "author": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "reason": "", + "created_on": "2023-03-08T06:10:12.744328+00:00", + "updated_on": "2023-03-08T06:10:13.341510+00:00", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "type": "commit", + "hash": "0ccb80619bd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "source": { + "branch": { + "name": "new-branch" + }, + "commit": { + "type": "commit", + "hash": "0845504f4acb", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acb" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acb" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "reviewers": [], + "participants": [], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/commits" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/approve" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/request-changes" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diffstat/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/activity" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/merge" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/decline" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/statuses" + } + }, + "summary": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\n* anotherfile.md created online with Bitbucket\n* thirdfile created online with Bitbucket\n\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "3d9c67a2d1fee9ed", + "x-b3-traceid": "3d9c67a2d1fee9ed", + "x-event-key": "pullrequest:created", + "x-event-time": "Wed, 08 Mar 2023 06:10:13 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "3868d64c-e550-4e36-b44d-6ee67f6d4d52" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "pullrequest:fulfilled", + "name": "pullrequest:fulfilled", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "pullrequest": { + "comment_count": 1, + "task_count": 0, + "type": "pullrequest", + "id": 1, + "title": "New changes-multiple", + "description": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "rendered": { + "title": { + "type": "rendered", + "raw": "New changes-multiple", + "markup": "markdown", + "html": "

New changes-multiple

" + }, + "description": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "state": "MERGED", + "merge_commit": { + "type": "commit", + "hash": "ed2903eec378", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/ed2903eec378" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/ed2903eec378" + } + } + }, + "close_source_branch": false, + "closed_by": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "author": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "reason": "", + "created_on": "2023-03-08T06:10:12.744328+00:00", + "updated_on": "2023-03-08T06:12:22.485416+00:00", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "type": "commit", + "hash": "0ccb80619bd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "source": { + "branch": { + "name": "new-branch" + }, + "commit": { + "type": "commit", + "hash": "0845504f4acb", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acb" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acb" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "reviewers": [], + "participants": [ + { + "type": "participant", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "role": "PARTICIPANT", + "approved": true, + "state": "approved", + "participated_on": "2023-03-08T06:12:10.654730+00:00" + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/commits" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/approve" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/request-changes" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diffstat/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/activity" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/merge" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/decline" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/statuses" + } + }, + "summary": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "5490b6d67b4379e7", + "x-b3-traceid": "5490b6d67b4379e7", + "x-event-key": "pullrequest:fulfilled", + "x-event-time": "Wed, 08 Mar 2023 06:12:22 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "9236c48d-b51d-458e-8a75-42696866045a" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "pullrequest:unapproved", + "name": "pullrequest:unapproved", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "pullrequest": { + "comment_count": 1, + "task_count": 0, + "type": "pullrequest", + "id": 1, + "title": "New changes-multiple", + "description": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "rendered": { + "title": { + "type": "rendered", + "raw": "New changes-multiple", + "markup": "markdown", + "html": "

New changes-multiple

" + }, + "description": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "state": "OPEN", + "merge_commit": null, + "close_source_branch": false, + "closed_by": null, + "author": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "reason": "", + "created_on": "2023-03-08T06:10:12.744328+00:00", + "updated_on": "2023-03-08T06:12:02.826901+00:00", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "type": "commit", + "hash": "0ccb80619bd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "source": { + "branch": { + "name": "new-branch" + }, + "commit": { + "type": "commit", + "hash": "0845504f4acb", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acb" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acb" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "reviewers": [], + "participants": [ + { + "type": "participant", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "role": "PARTICIPANT", + "approved": false, + "state": null, + "participated_on": "2023-03-08T06:11:02.138668+00:00" + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/commits" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/approve" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/request-changes" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diffstat/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/activity" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/merge" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/decline" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/statuses" + } + }, + "summary": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "approval": { + "date": "2023-03-08T06:11:26.895005+00:00", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "9d87ce69bfc31c90", + "x-b3-traceid": "9d87ce69bfc31c90", + "x-event-key": "pullrequest:unapproved", + "x-event-time": "Wed, 08 Mar 2023 06:12:02 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "27ec303b-67f6-488e-af5a-6d0d9ee3ee34" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "pullrequest:updated", + "name": "pullrequest:updated", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "pullrequest": { + "comment_count": 1, + "task_count": 0, + "type": "pullrequest", + "id": 1, + "title": "New changes-multiple", + "description": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "rendered": { + "title": { + "type": "rendered", + "raw": "New changes-multiple", + "markup": "markdown", + "html": "

New changes-multiple

" + }, + "description": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + }, + "state": "OPEN", + "merge_commit": null, + "close_source_branch": false, + "closed_by": null, + "author": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "reason": "", + "created_on": "2023-03-08T06:10:12.744328+00:00", + "updated_on": "2023-03-08T06:11:47.499252+00:00", + "destination": { + "branch": { + "name": "master" + }, + "commit": { + "type": "commit", + "hash": "0ccb80619bd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "source": { + "branch": { + "name": "new-branch" + }, + "commit": { + "type": "commit", + "hash": "0845504f4acb", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acb" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acb" + } + } + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + }, + "reviewers": [], + "participants": [ + { + "type": "participant", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "role": "PARTICIPANT", + "approved": true, + "state": "approved", + "participated_on": "2023-03-08T06:11:26.895005+00:00" + } + ], + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/pull-requests/1" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/commits" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/approve" + }, + "request-changes": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/request-changes" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "diffstat": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diffstat/hookdeck-write/hookdeckwrite:0845504f4acb%0D0ccb80619bd1?from_pullrequest_id=1&topic=true" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/comments" + }, + "activity": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/activity" + }, + "merge": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/merge" + }, + "decline": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/decline" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/pullrequests/1/statuses" + } + }, + "summary": { + "type": "rendered", + "raw": "* testfile.md created online with Bitbucket\r\n* anotherfile.md created online with Bitbucket\r\n* thirdfile created online with Bitbucket\r\n\r\n‌", + "markup": "markdown", + "html": "
    \n
  • testfile.md created online with Bitbucket
  • \n
  • anotherfile.md created online with Bitbucket
  • \n
  • thirdfile created online with Bitbucket
  • \n
\n

" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "004d072a2d7b9f7f", + "x-b3-traceid": "004d072a2d7b9f7f", + "x-event-key": "pullrequest:updated", + "x-event-time": "Wed, 08 Mar 2023 06:11:47 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "122c6e55-234e-41c9-aa50-964f75e3ec4b" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "repo:commit_comment_created", + "name": "repo:commit_comment_created", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "comment": { + "id": 13086145, + "created_on": "2023-03-08T06:09:37.857332+00:00", + "updated_on": "2023-03-08T06:09:37.858505+00:00", + "content": { + "type": "rendered", + "raw": "Marked", + "markup": "markdown", + "html": "

Marked

" + }, + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "deleted": false, + "inline": { + "from": null, + "to": null, + "path": "thirdfile" + }, + "type": "commit_comment", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04/comments/13086145" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acbfb5f953bda80bf37426adfbe6a04#comment-13086145" + }, + "code": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/0845504f4acbfb5f953bda80bf37426adfbe6a04?path=thirdfile" + } + }, + "commit": { + "type": "commit", + "hash": "0845504f4acbfb5f953bda80bf37426adfbe6a04", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acbfb5f953bda80bf37426adfbe6a04" + } + } + } + }, + "commit": { + "type": "base_commit", + "hash": "0845504f4acbfb5f953bda80bf37426adfbe6a04", + "message": "thirdfile created online with Bitbucket", + "links": {} + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "fbca366dc8f62bf4", + "x-b3-traceid": "fbca366dc8f62bf4", + "x-event-key": "repo:commit_comment_created", + "x-event-time": "Wed, 08 Mar 2023 06:09:37 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "e36480d4-e3f4-4fc7-9ebe-bffe878ebbd8" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "repo:fork", + "name": "repo:fork", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "fork": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite-forked", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite-forked" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B281881dc-bcc2-4407-88d4-2e4098d6a197%7D?ts=default" + }, + "pullrequests": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/pullrequests" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/commits" + }, + "forks": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/forks" + }, + "watchers": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/watchers" + }, + "branches": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/refs/branches" + }, + "tags": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/refs/tags" + }, + "downloads": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/downloads" + }, + "source": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite-forked/src" + }, + "clone": [ + { + "name": "https", + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite-forked.git" + }, + { + "name": "ssh", + "href": "git@bitbucket.org:hookdeck-write/hookdeckwrite-forked.git" + } + ] + }, + "name": "HookdeckWrite-Forked", + "slug": "hookdeckwrite-forked", + "description": "", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "FOR", + "uuid": "{f1d57457-480d-42ac-9a42-c8ea71141bbb}", + "name": "forked-repo", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/FOR" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/FOR" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/FOR/avatar/32?ts=1678220494" + } + } + }, + "fork_policy": "allow_forks", + "created_on": "2023-03-07T20:21:34.640720+00:00", + "updated_on": "2023-03-07T20:21:34.724628+00:00", + "size": 0, + "language": "", + "has_issues": false, + "has_wiki": false, + "uuid": "{281881dc-bcc2-4407-88d4-2e4098d6a197}", + "mainbranch": {}, + "override_settings": {}, + "properties": {}, + "state": null, + "parent": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "cc57bfb9b2820509", + "x-b3-traceid": "cc57bfb9b2820509", + "x-event-key": "repo:fork", + "x-event-time": "Tue, 07 Mar 2023 20:21:34 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "452bcde8-0f27-4af6-b1ad-34747cc723f6" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "repo:push", + "name": "repo:push", + "body": { + "push": { + "changes": [ + { + "old": { + "name": "master", + "target": { + "type": "commit", + "hash": "0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc", + "date": "2023-02-05T18:43:11+00:00", + "author": { + "type": "author", + "raw": "Hookdeck Write ", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "message": "README.md edited online with Bitbucket", + "summary": { + "type": "rendered", + "raw": "README.md edited online with Bitbucket", + "markup": "markdown", + "html": "

README.md edited online with Bitbucket

" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + } + }, + "parents": [ + { + "type": "commit", + "hash": "f4c075df4f62630dd0e65ae41f0fb4d221018fd1", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/f4c075df4f62630dd0e65ae41f0fb4d221018fd1" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/f4c075df4f62630dd0e65ae41f0fb4d221018fd1" + } + } + } + ], + "rendered": {}, + "properties": {} + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/refs/branches/master" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commits/master" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/branch/master" + } + }, + "type": "branch", + "merge_strategies": [ + "merge_commit", + "squash", + "fast_forward" + ], + "default_merge_strategy": "merge_commit" + }, + "new": { + "name": "master", + "target": { + "type": "commit", + "hash": "ed2903eec378fe5d4aa389b656420ccd9d5e723d", + "date": "2023-03-08T06:12:18+00:00", + "author": { + "type": "author", + "raw": "Hookdeck Write ", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "message": "Merged in new-branch (pull request #1)\n\nNew changes-multiple", + "summary": { + "type": "rendered", + "raw": "Merged in new-branch (pull request #1)\n\nNew changes-multiple", + "markup": "markdown", + "html": "

Merged in new-branch (pull request #1)

\n

New changes-multiple

" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/ed2903eec378fe5d4aa389b656420ccd9d5e723d" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/ed2903eec378fe5d4aa389b656420ccd9d5e723d" + } + }, + "parents": [ + { + "type": "commit", + "hash": "0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + } + } + }, + { + "type": "commit", + "hash": "0845504f4acbfb5f953bda80bf37426adfbe6a04", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acbfb5f953bda80bf37426adfbe6a04" + } + } + } + ], + "rendered": {}, + "properties": {} + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/refs/branches/master" + }, + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commits/master" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/branch/master" + } + }, + "type": "branch", + "merge_strategies": [ + "merge_commit", + "squash", + "fast_forward" + ], + "default_merge_strategy": "merge_commit" + }, + "truncated": false, + "created": false, + "forced": false, + "closed": false, + "links": { + "commits": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commits?include=ed2903eec378fe5d4aa389b656420ccd9d5e723d&exclude=0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/ed2903eec378fe5d4aa389b656420ccd9d5e723d..0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/branches/compare/ed2903eec378fe5d4aa389b656420ccd9d5e723d..0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + } + }, + "commits": [ + { + "type": "commit", + "hash": "ed2903eec378fe5d4aa389b656420ccd9d5e723d", + "date": "2023-03-08T06:12:18+00:00", + "author": { + "type": "author", + "raw": "Hookdeck Write ", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "message": "Merged in new-branch (pull request #1)\n\nNew changes-multiple", + "summary": { + "type": "rendered", + "raw": "Merged in new-branch (pull request #1)\n\nNew changes-multiple", + "markup": "markdown", + "html": "

Merged in new-branch (pull request #1)

\n

New changes-multiple

" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/ed2903eec378fe5d4aa389b656420ccd9d5e723d" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/ed2903eec378fe5d4aa389b656420ccd9d5e723d" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/ed2903eec378fe5d4aa389b656420ccd9d5e723d" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/ed2903eec378fe5d4aa389b656420ccd9d5e723d/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/ed2903eec378fe5d4aa389b656420ccd9d5e723d/comments" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/ed2903eec378fe5d4aa389b656420ccd9d5e723d/statuses" + } + }, + "parents": [ + { + "type": "commit", + "hash": "0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + } + } + }, + { + "type": "commit", + "hash": "0845504f4acbfb5f953bda80bf37426adfbe6a04", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acbfb5f953bda80bf37426adfbe6a04" + } + } + } + ], + "rendered": {}, + "properties": {} + }, + { + "type": "commit", + "hash": "0845504f4acbfb5f953bda80bf37426adfbe6a04", + "date": "2023-03-08T06:08:37+00:00", + "author": { + "type": "author", + "raw": "Hookdeck Write ", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "message": "thirdfile created online with Bitbucket", + "summary": { + "type": "rendered", + "raw": "thirdfile created online with Bitbucket", + "markup": "markdown", + "html": "

thirdfile created online with Bitbucket

" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0845504f4acbfb5f953bda80bf37426adfbe6a04" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/0845504f4acbfb5f953bda80bf37426adfbe6a04" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04/comments" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0845504f4acbfb5f953bda80bf37426adfbe6a04/statuses" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/patch/0845504f4acbfb5f953bda80bf37426adfbe6a04" + } + }, + "parents": [ + { + "type": "commit", + "hash": "a219c19cdff4dbd357732e29f3010ae143e86575", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/a219c19cdff4dbd357732e29f3010ae143e86575" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/a219c19cdff4dbd357732e29f3010ae143e86575" + } + } + } + ], + "rendered": {}, + "properties": {} + }, + { + "type": "commit", + "hash": "a219c19cdff4dbd357732e29f3010ae143e86575", + "date": "2023-03-07T20:57:17+00:00", + "author": { + "type": "author", + "raw": "Hookdeck Write ", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "message": "anotherfile.md created online with Bitbucket", + "summary": { + "type": "rendered", + "raw": "anotherfile.md created online with Bitbucket", + "markup": "markdown", + "html": "

anotherfile.md created online with Bitbucket

" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/a219c19cdff4dbd357732e29f3010ae143e86575" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/a219c19cdff4dbd357732e29f3010ae143e86575" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/a219c19cdff4dbd357732e29f3010ae143e86575" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/a219c19cdff4dbd357732e29f3010ae143e86575/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/a219c19cdff4dbd357732e29f3010ae143e86575/comments" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/a219c19cdff4dbd357732e29f3010ae143e86575/statuses" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/patch/a219c19cdff4dbd357732e29f3010ae143e86575" + } + }, + "parents": [ + { + "type": "commit", + "hash": "81ba7a30508ff62ffb7e4b3c3101d4bf29212331", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/81ba7a30508ff62ffb7e4b3c3101d4bf29212331" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/81ba7a30508ff62ffb7e4b3c3101d4bf29212331" + } + } + } + ], + "rendered": {}, + "properties": {} + }, + { + "type": "commit", + "hash": "81ba7a30508ff62ffb7e4b3c3101d4bf29212331", + "date": "2023-03-07T20:38:47+00:00", + "author": { + "type": "author", + "raw": "Hookdeck Write ", + "user": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "message": "testfile.md created online with Bitbucket", + "summary": { + "type": "rendered", + "raw": "testfile.md created online with Bitbucket", + "markup": "markdown", + "html": "

testfile.md created online with Bitbucket

" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/81ba7a30508ff62ffb7e4b3c3101d4bf29212331" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/81ba7a30508ff62ffb7e4b3c3101d4bf29212331" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/diff/81ba7a30508ff62ffb7e4b3c3101d4bf29212331" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/81ba7a30508ff62ffb7e4b3c3101d4bf29212331/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/81ba7a30508ff62ffb7e4b3c3101d4bf29212331/comments" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/81ba7a30508ff62ffb7e4b3c3101d4bf29212331/statuses" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/patch/81ba7a30508ff62ffb7e4b3c3101d4bf29212331" + } + }, + "parents": [ + { + "type": "commit", + "hash": "0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite/commit/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite/commits/0ccb80619bd1a8d7d040e84fd1c745ad10c0e8fc" + } + } + } + ], + "rendered": {}, + "properties": {} + } + ] + } + ] + }, + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "d10ac0c40964b71e", + "x-b3-traceid": "d10ac0c40964b71e", + "x-event-key": "repo:push", + "x-event-time": "Wed, 08 Mar 2023 06:12:20 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "831ba044-6864-4085-bf4c-ae829079e224" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "bitbucket", + "providerLabel": "Bitbucket", + "eventType": "repo:updated", + "name": "repo:updated", + "body": { + "repository": { + "type": "repository", + "full_name": "hookdeck-write/hookdeckwrite", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hookdeck-write/hookdeckwrite" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/hookdeckwrite" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B7c9a2905-962d-4fb1-83a1-d316012f033a%7D?ts=default" + } + }, + "name": "HookdeckWrite", + "scm": "git", + "website": null, + "owner": { + "display_name": "Hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D" + }, + "avatar": { + "href": "https://bitbucket.org/account/hookdeck-write/avatar/" + }, + "html": { + "href": "https://bitbucket.org/%7Ba217ee3e-d18c-4f19-8ebd-82576d644050%7D/" + } + }, + "type": "team", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "username": "hookdeck-write" + }, + "workspace": { + "type": "workspace", + "uuid": "{a217ee3e-d18c-4f19-8ebd-82576d644050}", + "name": "Hookdeck-write", + "slug": "hookdeck-write", + "links": { + "avatar": { + "href": "https://bitbucket.org/workspaces/hookdeck-write/avatar/?ts=1675403623" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write" + } + } + }, + "is_private": true, + "project": { + "type": "project", + "key": "HOOK", + "uuid": "{77c3c8c2-af98-4ce9-9e53-7d1d3dbfacb5}", + "name": "hookdeck-write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/workspaces/hookdeck-write/projects/HOOK" + }, + "html": { + "href": "https://bitbucket.org/hookdeck-write/workspace/projects/HOOK" + }, + "avatar": { + "href": "https://bitbucket.org/account/user/hookdeck-write/projects/HOOK/avatar/32?ts=1675403694" + } + } + }, + "uuid": "{7c9a2905-962d-4fb1-83a1-d316012f033a}" + }, + "actor": { + "display_name": "Hookdeck Write", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D" + }, + "avatar": { + "href": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FHW-5.png" + }, + "html": { + "href": "https://bitbucket.org/%7B72df0fc8-d124-495a-aa95-c8e9d5cb2001%7D/" + } + }, + "type": "user", + "uuid": "{72df0fc8-d124-495a-aa95-c8e9d5cb2001}", + "account_id": "63dc9fbb614cb4ba53000891", + "nickname": "Hookdeck Write" + }, + "changes": { + "description": { + "old": "New description", + "new": "Simple Webhook description" + } + } + }, + "extraHeaders": { + "x-attempt-number": "1", + "x-b3-sampled": "1", + "x-b3-spanid": "0f80e24b3f265467", + "x-b3-traceid": "0f80e24b3f265467", + "x-event-key": "repo:updated", + "x-event-time": "Tue, 07 Mar 2023 20:35:15 GMT", + "x-hook-uuid": "91e2a5e1-d608-4a66-bda6-db4e1590b209", + "x-request-uuid": "4bc23bf8-70ac-44da-b2b4-4f5009b1e83b" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:bitbucket/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "checkout", + "providerLabel": "Checkout.com", + "eventType": "authentication_approved", + "name": "authentication_approved", + "body": { + "id": "evt_ewzytrdo7n6udniblrzute5an4", + "type": "authentication_approved", + "version": "1.0.1", + "created_on": "2023-03-03T08:31:04.55Z", + "data": { + "eci": "05", + "cavv": "AAABAVIREQAAAAAAAAAAAAAAAAA=", + "xid": "4b4f21d8-6bd5-4359-9edd-ae7d120b381b", + "challenged": false, + "acs_challenged_mandated": false, + "exemption_applied": "none", + "session_id": "fe4c0bc2-d7ad-4163-bde3-8b39ead240f9", + "amount": "88000", + "currency": "EUR", + "type": "regular", + "challenge_indicator": "no_preference", + "protocol_version": "2.1.0", + "scheme": "visa" + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/workflows/events/evt_ewzytrdo7n6udniblrzute5an4" + } + } + }, + "extraHeaders": { + "cko-correlation-id": "661c3b59-5dde-4e6b-92fa-c9eabbd80086", + "cko-invocation-id": "ivc_v7x7d2hgcizezdzkjywufw6ueq" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:checkout/1.0.25", + "snapshotDate": "2026-07" + } + }, + { + "provider": "checkout", + "providerLabel": "Checkout.com", + "eventType": "authentication_started", + "name": "authentication_started", + "body": { + "id": "evt_junw6auqvppe5efrknbdfpba6i", + "type": "authentication_started", + "version": "1.0.1", + "created_on": "2023-03-03T08:30:49.37Z", + "data": { + "session_id": "fe4c0bc2-d7ad-4163-bde3-8b39ead240f9", + "amount": "88000", + "currency": "EUR", + "type": "regular", + "challenge_indicator": "no_preference", + "protocol_version": "2.1.0", + "scheme": "visa" + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/workflows/events/evt_junw6auqvppe5efrknbdfpba6i" + } + } + }, + "extraHeaders": { + "cko-correlation-id": "661c3b59-5dde-4e6b-92fa-c9eabbd80086", + "cko-invocation-id": "ivc_mdfh4hz4k3qudajh52muhtctqu" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:checkout/1.0.25", + "snapshotDate": "2026-07" + } + }, + { + "provider": "checkout", + "providerLabel": "Checkout.com", + "eventType": "payment_approved", + "name": "payment_approved", + "body": { + "id": "evt_htpp6niiutrejjz5dov7pm46hy", + "type": "payment_approved", + "version": "1.0.26", + "created_on": "2023-03-03T08:31:14.4942053Z", + "data": { + "id": "pay_oogtk3sugdxu7bilm4dbvzlmcy", + "action_id": "act_gforaglshmeu3nwqtl43henpny", + "amount": 88000, + "auth_code": "651587", + "currency": "EUR", + "metadata": { + "correlation_id": "c6a4f3f8-bf9b-419c-82d8-d5716877196e" + }, + "payment_type": "Regular", + "processed_on": "2023-03-03T08:31:05.4892639Z", + "processing": { + "acquirer_transaction_id": "280056362716772951030", + "retrieval_reference_number": "578261409697" + }, + "response_code": "10000", + "response_summary": "Approved", + "risk": { + "flagged": false + }, + "3ds": { + "version": "2.1.0", + "challenged": false, + "challenge_indicator": "no_preference", + "exemption": "none", + "eci": "05", + "cavv": "AAABAVIREQAAAAAAAAAAAAAAAAA=", + "xid": "fe4c0bc2-d7ad-4163-bde3-8b39ead240f9", + "downgraded": false, + "enrolled": "Y", + "authentication_response": "Y", + "flow_type": "frictionless" + }, + "scheme_id": "777051524135103", + "source": { + "id": "src_lkpmbqvcdbzutd23xm3hwp7cdi", + "type": "card", + "billing_address": { + "country": "GB" + }, + "expiry_month": 12, + "expiry_year": 2023, + "scheme": "VISA", + "last_4": "0685", + "fingerprint": "9629E3BB7D3888D1C6F7D2517B44332FF09B778B825684D5A4E76972A8A13961", + "bin": "465995", + "card_type": "CREDIT", + "card_category": "CONSUMER", + "issuer": "TELLER, A.S.", + "issuer_country": "NO", + "product_type": "PLATINUM", + "avs_check": "G", + "cvv_check": "Y" + }, + "balances": { + "total_authorized": 88000, + "total_voided": 0, + "available_to_void": 88000, + "total_captured": 0, + "available_to_capture": 88000, + "total_refunded": 0, + "available_to_refund": 0 + }, + "event_links": { + "payment": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy", + "payment_actions": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy/actions", + "capture": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy/captures", + "void": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy/voids" + } + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/workflows/events/evt_htpp6niiutrejjz5dov7pm46hy" + }, + "subject": { + "href": "https://api.sandbox.checkout.com/workflows/events/subject/pay_oogtk3sugdxu7bilm4dbvzlmcy" + }, + "payment": { + "href": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy" + }, + "payment_actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy/actions" + }, + "capture": { + "href": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy/captures" + }, + "void": { + "href": "https://api.sandbox.checkout.com/payments/pay_oogtk3sugdxu7bilm4dbvzlmcy/voids" + } + } + }, + "extraHeaders": { + "cko-correlation-id": "661c3b59-5dde-4e6b-92fa-c9eabbd80086", + "cko-invocation-id": "ivc_2hqbbp6bg2lejjaciqq67sddiy" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:checkout/1.0.25", + "snapshotDate": "2026-07" + } + }, + { + "provider": "checkout", + "providerLabel": "Checkout.com", + "eventType": "payment_captured", + "name": "payment_captured", + "body": { + "id": "evt_uiewxipcyhvefbvc3skk7fpqfq", + "type": "payment_captured", + "version": "1.0.26", + "created_on": "2023-03-03T08:18:57.8376703Z", + "data": { + "id": "pay_ht4n7ohvpc7epirpfco6wdou24", + "action_id": "act_24iebe4b2atuhj4iemx7a4dx6q", + "amount": 12000, + "processed_on": "2023-03-03T08:18:52.1378682Z", + "response_code": "10000", + "response_summary": "Approved", + "balances": { + "total_authorized": 12000, + "total_voided": 0, + "available_to_void": 0, + "total_captured": 12000, + "available_to_capture": 0, + "total_refunded": 0, + "available_to_refund": 12000 + }, + "metadata": { + "correlation_id": "bb17cc43-3827-4190-ab70-d374244afac0" + }, + "currency": "EUR", + "processing": { + "acquirer_transaction_id": "141625565560457107263", + "acquirer_reference_number": "33255926420936831486570" + }, + "event_links": { + "payment": "https://api.sandbox.checkout.com/payments/pay_ht4n7ohvpc7epirpfco6wdou24", + "payment_actions": "https://api.sandbox.checkout.com/payments/pay_ht4n7ohvpc7epirpfco6wdou24/actions", + "refund": "https://api.sandbox.checkout.com/payments/pay_ht4n7ohvpc7epirpfco6wdou24/refunds" + } + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/workflows/events/evt_uiewxipcyhvefbvc3skk7fpqfq" + }, + "subject": { + "href": "https://api.sandbox.checkout.com/workflows/events/subject/pay_ht4n7ohvpc7epirpfco6wdou24" + }, + "payment": { + "href": "https://api.sandbox.checkout.com/payments/pay_ht4n7ohvpc7epirpfco6wdou24" + }, + "payment_actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_ht4n7ohvpc7epirpfco6wdou24/actions" + }, + "refund": { + "href": "https://api.sandbox.checkout.com/payments/pay_ht4n7ohvpc7epirpfco6wdou24/refunds" + } + } + }, + "extraHeaders": { + "cko-correlation-id": "74432c84-df9d-43d7-9a04-869c315d2d9e", + "cko-invocation-id": "ivc_5uo3ctib3fbeja3ye6yjquezfy" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:checkout/1.0.25", + "snapshotDate": "2026-07" + } + }, + { + "provider": "checkout", + "providerLabel": "Checkout.com", + "eventType": "payment_voided", + "name": "payment_voided", + "body": { + "id": "evt_c2oy4t4w7s7u5ebyx2ujy2fbg4", + "type": "payment_voided", + "version": "1.0.26", + "created_on": "2023-03-03T08:17:12.3289437Z", + "data": { + "id": "pay_rvaqaesek3eejfly4gt7d7eopa", + "action_id": "act_vsbujgvslqhupiaph24hpjfhwq", + "amount": 6000, + "processed_on": "2023-03-03T08:17:06.0517435Z", + "response_code": "10000", + "response_summary": "Approved", + "balances": { + "total_authorized": 6000, + "total_voided": 6000, + "available_to_void": 0, + "total_captured": 0, + "available_to_capture": 0, + "total_refunded": 0, + "available_to_refund": 0 + }, + "metadata": { + "correlation_id": "20c627d6-e909-43db-8f29-b443379b3b60" + }, + "currency": "EUR", + "event_links": { + "payment": "https://api.sandbox.checkout.com/payments/pay_rvaqaesek3eejfly4gt7d7eopa", + "payment_actions": "https://api.sandbox.checkout.com/payments/pay_rvaqaesek3eejfly4gt7d7eopa/actions" + } + }, + "_links": { + "self": { + "href": "https://api.sandbox.checkout.com/workflows/events/evt_c2oy4t4w7s7u5ebyx2ujy2fbg4" + }, + "subject": { + "href": "https://api.sandbox.checkout.com/workflows/events/subject/pay_rvaqaesek3eejfly4gt7d7eopa" + }, + "payment": { + "href": "https://api.sandbox.checkout.com/payments/pay_rvaqaesek3eejfly4gt7d7eopa" + }, + "payment_actions": { + "href": "https://api.sandbox.checkout.com/payments/pay_rvaqaesek3eejfly4gt7d7eopa/actions" + } + } + }, + "extraHeaders": { + "cko-correlation-id": "a820d422-879d-4542-b316-a78bc3920bd3", + "cko-invocation-id": "ivc_hgwdghg4ajdehpsjmmeazeb2de" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:checkout/1.0.25", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "customers.create", + "name": "customers.create", + "body": { + "data": { + "id": "njmyhybwre", + "type": "customers", + "links": { + "self": "/api/customers/njmyhybwre" + }, + "attributes": { + "email": "creted.cus@mail.com", + "status": "prospect", + "has_password": true, + "created_at": "2023-03-17T10:17:18.679Z", + "updated_at": "2023-03-17T10:17:18.679Z", + "reference": "", + "reference_origin": "", + "metadata": {} + }, + "relationships": { + "customer_group": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/customer_group", + "related": "/api/customers/njmyhybwre/customer_group" + } + }, + "customer_addresses": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/customer_addresses", + "related": "/api/customers/njmyhybwre/customer_addresses" + } + }, + "customer_payment_sources": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/customer_payment_sources", + "related": "/api/customers/njmyhybwre/customer_payment_sources" + } + }, + "customer_subscriptions": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/customer_subscriptions", + "related": "/api/customers/njmyhybwre/customer_subscriptions" + } + }, + "orders": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/orders", + "related": "/api/customers/njmyhybwre/orders" + } + }, + "order_subscriptions": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/order_subscriptions", + "related": "/api/customers/njmyhybwre/order_subscriptions" + } + }, + "returns": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/returns", + "related": "/api/customers/njmyhybwre/returns" + } + }, + "sku_lists": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/sku_lists", + "related": "/api/customers/njmyhybwre/sku_lists" + } + }, + "attachments": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/attachments", + "related": "/api/customers/njmyhybwre/attachments" + } + }, + "events": { + "links": { + "self": "/api/customers/njmyhybwre/relationships/events", + "related": "/api/customers/njmyhybwre/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6IjQyZTgzMGFkYTQ5NGMwNDUiLCJ0ciI6ImU2Y2UwMGZjZjVkNDczOWNlOWZlMzUxMWI2OTcyZGVkIiwic2EiOmZhbHNlLCJwciI6MC45OTIwNzksInRpIjoxNjc5MDQ4MjM4NzA0LCJpZCI6IjE2MzI4MTZjYjVhZmU3ZWYifX0=", + "tracestate": "2097276@nr=0-0-2097276-83882605-1632816cb5afe7ef-42e830ada494c045-0-0.992079-1679048238704", + "x-commercelayer-topic": "customers.create" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "gift_cards.activate", + "name": "gift_cards.activate", + "body": { + "data": { + "id": "lEnKUPNBQD", + "type": "gift_cards", + "links": { + "self": "/api/gift_cards/lEnKUPNBQD" + }, + "attributes": { + "status": "active", + "code": "1584b1f2-f31e-4154-b6a7-1aac408d918e", + "currency_code": "USD", + "initial_balance_cents": 995, + "initial_balance_float": 9.95, + "formatted_initial_balance": "$9.95", + "balance_cents": 995, + "balance_float": 9.95, + "formatted_balance": "$9.95", + "balance_max_cents": 2000, + "balance_max_float": 20, + "formatted_balance_max": "$20.00", + "balance_log": [ + { + "datetime": "2023-03-17T10:34:46.786Z", + "balance_change_cents": 995 + } + ], + "single_use": false, + "rechargeable": true, + "image_url": "", + "expires_at": null, + "recipient_email": "pokaka7480@asoflex.com", + "created_at": "2023-03-17T10:34:46.786Z", + "updated_at": "2023-03-17T10:37:35.257Z", + "reference": "gifted", + "reference_origin": "bonus", + "metadata": {} + }, + "relationships": { + "market": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/market", + "related": "/api/gift_cards/lEnKUPNBQD/market" + } + }, + "gift_card_recipient": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/gift_card_recipient", + "related": "/api/gift_cards/lEnKUPNBQD/gift_card_recipient" + } + }, + "attachments": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/attachments", + "related": "/api/gift_cards/lEnKUPNBQD/attachments" + } + }, + "events": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/events", + "related": "/api/gift_cards/lEnKUPNBQD/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6IjBlYWQxOGU0NDk0YTYyOWIiLCJ0ciI6IjJhNzQ1OTI2MDE3NDMwNzkxZmVhM2JmNTU1NjhmMDIwIiwic2EiOmZhbHNlLCJwciI6MC4xMDM0NzksInRpIjoxNjc5MDQ5NDU1MjcyLCJpZCI6ImRhZTdmOWVkMjMyZWMyNTEifX0=", + "tracestate": "2097276@nr=0-0-2097276-83882605-dae7f9ed232ec251-0ead18e4494a629b-0-0.103479-1679049455272", + "x-commercelayer-topic": "gift_cards.activate" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "gift_cards.create", + "name": "gift_cards.create", + "body": { + "data": { + "id": "lEnKUPNBQD", + "type": "gift_cards", + "links": { + "self": "/api/gift_cards/lEnKUPNBQD" + }, + "attributes": { + "status": "draft", + "code": "1584b1f2-f31e-4154-b6a7-1aac408d918e", + "currency_code": "USD", + "initial_balance_cents": 995, + "initial_balance_float": 9.95, + "formatted_initial_balance": "$9.95", + "balance_cents": 995, + "balance_float": 9.95, + "formatted_balance": "$9.95", + "balance_max_cents": 2000, + "balance_max_float": 20, + "formatted_balance_max": "$20.00", + "balance_log": [ + { + "datetime": "2023-03-17T10:34:46.786Z", + "balance_change_cents": 995 + } + ], + "single_use": false, + "rechargeable": true, + "image_url": "", + "expires_at": null, + "recipient_email": "pokaka7480@asoflex.com", + "created_at": "2023-03-17T10:34:46.786Z", + "updated_at": "2023-03-17T10:34:46.786Z", + "reference": "gifted", + "reference_origin": "bonus", + "metadata": {} + }, + "relationships": { + "market": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/market", + "related": "/api/gift_cards/lEnKUPNBQD/market" + } + }, + "gift_card_recipient": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/gift_card_recipient", + "related": "/api/gift_cards/lEnKUPNBQD/gift_card_recipient" + } + }, + "attachments": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/attachments", + "related": "/api/gift_cards/lEnKUPNBQD/attachments" + } + }, + "events": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/events", + "related": "/api/gift_cards/lEnKUPNBQD/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6IjMzZDA5ZTMyN2EzMGNhNTIiLCJ0ciI6IjQ2ZDk1ZDJmZmU4ZDRhMjA2YWZjZjY5YjQ3ODYyMjFlIiwic2EiOmZhbHNlLCJwciI6MC42MDg5NjUsInRpIjoxNjc5MDQ5Mjg2ODE1LCJpZCI6ImJhMjBiNGUwNTExY2MwY2UifX0=", + "tracestate": "2097276@nr=0-0-2097276-83882605-ba20b4e0511cc0ce-33d09e327a30ca52-0-0.608965-1679049286815", + "x-commercelayer-topic": "gift_cards.create" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "gift_cards.deactivate", + "name": "gift_cards.deactivate", + "body": { + "data": { + "id": "lEnKUPNBQD", + "type": "gift_cards", + "links": { + "self": "/api/gift_cards/lEnKUPNBQD" + }, + "attributes": { + "status": "inactive", + "code": "1584b1f2-f31e-4154-b6a7-1aac408d918e", + "currency_code": "USD", + "initial_balance_cents": 995, + "initial_balance_float": 9.95, + "formatted_initial_balance": "$9.95", + "balance_cents": 995, + "balance_float": 9.95, + "formatted_balance": "$9.95", + "balance_max_cents": 2000, + "balance_max_float": 20, + "formatted_balance_max": "$20.00", + "balance_log": [ + { + "datetime": "2023-03-17T10:34:46.786Z", + "balance_change_cents": 995 + } + ], + "single_use": false, + "rechargeable": true, + "image_url": "", + "expires_at": null, + "recipient_email": "pokaka7480@asoflex.com", + "created_at": "2023-03-17T10:34:46.786Z", + "updated_at": "2023-03-17T10:38:48.325Z", + "reference": "gifted", + "reference_origin": "bonus", + "metadata": {} + }, + "relationships": { + "market": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/market", + "related": "/api/gift_cards/lEnKUPNBQD/market" + } + }, + "gift_card_recipient": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/gift_card_recipient", + "related": "/api/gift_cards/lEnKUPNBQD/gift_card_recipient" + } + }, + "attachments": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/attachments", + "related": "/api/gift_cards/lEnKUPNBQD/attachments" + } + }, + "events": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/events", + "related": "/api/gift_cards/lEnKUPNBQD/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6ImM2NDVjOGU5MjNhZWEzYzkiLCJ0ciI6IjJjMjQxN2Y2NjE1ZjJhZTg3Y2QzNzkzYmE3OGQxYTg3Iiwic2EiOmZhbHNlLCJwciI6MC4xNzMwNDEsInRpIjoxNjc5MDQ5NTI4MzM5LCJpZCI6IjEzMzk1M2JhNmI3M2VjZTUifX0=", + "tracestate": "2097276@nr=0-0-2097276-83882605-133953ba6b73ece5-c645c8e923aea3c9-0-0.173041-1679049528339", + "x-commercelayer-topic": "gift_cards.deactivate" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "gift_cards.purchase", + "name": "gift_cards.purchase", + "body": { + "data": { + "id": "lEnKUPNBQD", + "type": "gift_cards", + "links": { + "self": "/api/gift_cards/lEnKUPNBQD" + }, + "attributes": { + "status": "inactive", + "code": "1584b1f2-f31e-4154-b6a7-1aac408d918e", + "currency_code": "USD", + "initial_balance_cents": 995, + "initial_balance_float": 9.95, + "formatted_initial_balance": "$9.95", + "balance_cents": 995, + "balance_float": 9.95, + "formatted_balance": "$9.95", + "balance_max_cents": 2000, + "balance_max_float": 20, + "formatted_balance_max": "$20.00", + "balance_log": [ + { + "datetime": "2023-03-17T10:34:46.786Z", + "balance_change_cents": 995 + } + ], + "single_use": false, + "rechargeable": true, + "image_url": "", + "expires_at": null, + "recipient_email": "pokaka7480@asoflex.com", + "created_at": "2023-03-17T10:34:46.786Z", + "updated_at": "2023-03-17T10:36:40.682Z", + "reference": "gifted", + "reference_origin": "bonus", + "metadata": {} + }, + "relationships": { + "market": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/market", + "related": "/api/gift_cards/lEnKUPNBQD/market" + } + }, + "gift_card_recipient": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/gift_card_recipient", + "related": "/api/gift_cards/lEnKUPNBQD/gift_card_recipient" + } + }, + "attachments": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/attachments", + "related": "/api/gift_cards/lEnKUPNBQD/attachments" + } + }, + "events": { + "links": { + "self": "/api/gift_cards/lEnKUPNBQD/relationships/events", + "related": "/api/gift_cards/lEnKUPNBQD/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6IjYwMDY5ZDliOGZiOWI1ODIiLCJ0ciI6Ijk3NzYwMmQ0MGUzMzljNjQwZjQyMGVkMzk0Y2FkMTFkIiwic2EiOmZhbHNlLCJwciI6MC44Mzc2NiwidGkiOjE2NzkwNDk0MDA2OTUsImlkIjoiN2MyOTBmMWVhN2Q2YzIwNyJ9fQ==", + "tracestate": "2097276@nr=0-0-2097276-83882605-7c290f1ea7d6c207-60069d9b8fb9b582-0-0.837660-1679049400695", + "x-commercelayer-topic": "gift_cards.purchase" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "orders.create", + "name": "orders.create", + "body": { + "data": { + "id": "PlalhWGrAb", + "type": "orders", + "links": { + "self": "/api/orders/PlalhWGrAb" + }, + "attributes": { + "number": 35310566, + "autorefresh": true, + "status": "draft", + "payment_status": "unpaid", + "fulfillment_status": "unfulfilled", + "guest": false, + "editable": true, + "customer_email": "mark@mail.com", + "language_code": "en", + "currency_code": "USD", + "tax_included": false, + "tax_rate": null, + "freight_taxable": null, + "requires_billing_info": false, + "country_code": null, + "shipping_country_code_lock": null, + "coupon_code": null, + "gift_card_code": null, + "gift_card_or_coupon_code": null, + "subtotal_amount_cents": 0, + "subtotal_amount_float": 0, + "formatted_subtotal_amount": "$0.00", + "shipping_amount_cents": 0, + "shipping_amount_float": 0, + "formatted_shipping_amount": "$0.00", + "payment_method_amount_cents": 0, + "payment_method_amount_float": 0, + "formatted_payment_method_amount": "$0.00", + "discount_amount_cents": 0, + "discount_amount_float": 0, + "formatted_discount_amount": "$0.00", + "adjustment_amount_cents": 0, + "adjustment_amount_float": 0, + "formatted_adjustment_amount": "$0.00", + "gift_card_amount_cents": 0, + "gift_card_amount_float": 0, + "formatted_gift_card_amount": "$0.00", + "total_tax_amount_cents": 0, + "total_tax_amount_float": 0, + "formatted_total_tax_amount": "$0.00", + "subtotal_tax_amount_cents": 0, + "subtotal_tax_amount_float": 0, + "formatted_subtotal_tax_amount": "$0.00", + "shipping_tax_amount_cents": 0, + "shipping_tax_amount_float": 0, + "formatted_shipping_tax_amount": "$0.00", + "payment_method_tax_amount_cents": 0, + "payment_method_tax_amount_float": 0, + "formatted_payment_method_tax_amount": "$0.00", + "adjustment_tax_amount_cents": 0, + "adjustment_tax_amount_float": 0, + "formatted_adjustment_tax_amount": "$0.00", + "total_amount_cents": 0, + "total_amount_float": 0, + "formatted_total_amount": "$0.00", + "total_taxable_amount_cents": 0, + "total_taxable_amount_float": 0, + "formatted_total_taxable_amount": "$0.00", + "subtotal_taxable_amount_cents": 0, + "subtotal_taxable_amount_float": 0, + "formatted_subtotal_taxable_amount": "$0.00", + "shipping_taxable_amount_cents": 0, + "shipping_taxable_amount_float": 0, + "formatted_shipping_taxable_amount": "$0.00", + "payment_method_taxable_amount_cents": 0, + "payment_method_taxable_amount_float": 0, + "formatted_payment_method_taxable_amount": "$0.00", + "adjustment_taxable_amount_cents": 0, + "adjustment_taxable_amount_float": 0, + "formatted_adjustment_taxable_amount": "$0.00", + "total_amount_with_taxes_cents": 0, + "total_amount_with_taxes_float": 0, + "formatted_total_amount_with_taxes": "$0.00", + "fees_amount_cents": 0, + "fees_amount_float": 0, + "formatted_fees_amount": "$0.00", + "duty_amount_cents": null, + "duty_amount_float": null, + "formatted_duty_amount": null, + "skus_count": 0, + "line_item_options_count": 0, + "shipments_count": 0, + "tax_calculations_count": 0, + "payment_source_details": null, + "token": "9b207dcee16313d78115c1ee48df4097", + "cart_url": null, + "return_url": null, + "terms_url": null, + "privacy_url": null, + "checkout_url": null, + "placed_at": null, + "approved_at": null, + "cancelled_at": null, + "payment_updated_at": null, + "fulfillment_updated_at": null, + "refreshed_at": null, + "archived_at": null, + "expires_at": "2023-05-17T10:27:09.826Z", + "subscription_created_at": null, + "created_at": "2023-03-17T10:27:09.819Z", + "updated_at": "2023-03-17T10:27:09.819Z", + "reference": "", + "reference_origin": "", + "metadata": {} + }, + "relationships": { + "market": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/market", + "related": "/api/orders/PlalhWGrAb/market" + } + }, + "customer": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/customer", + "related": "/api/orders/PlalhWGrAb/customer" + } + }, + "shipping_address": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/shipping_address", + "related": "/api/orders/PlalhWGrAb/shipping_address" + } + }, + "billing_address": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/billing_address", + "related": "/api/orders/PlalhWGrAb/billing_address" + } + }, + "available_payment_methods": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/available_payment_methods", + "related": "/api/orders/PlalhWGrAb/available_payment_methods" + } + }, + "available_customer_payment_sources": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/available_customer_payment_sources", + "related": "/api/orders/PlalhWGrAb/available_customer_payment_sources" + } + }, + "available_free_skus": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/available_free_skus", + "related": "/api/orders/PlalhWGrAb/available_free_skus" + } + }, + "available_free_bundles": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/available_free_bundles", + "related": "/api/orders/PlalhWGrAb/available_free_bundles" + } + }, + "payment_method": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/payment_method", + "related": "/api/orders/PlalhWGrAb/payment_method" + } + }, + "payment_source": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/payment_source", + "related": "/api/orders/PlalhWGrAb/payment_source" + } + }, + "line_items": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/line_items", + "related": "/api/orders/PlalhWGrAb/line_items" + } + }, + "shipments": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/shipments", + "related": "/api/orders/PlalhWGrAb/shipments" + } + }, + "transactions": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/transactions", + "related": "/api/orders/PlalhWGrAb/transactions" + } + }, + "authorizations": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/authorizations", + "related": "/api/orders/PlalhWGrAb/authorizations" + } + }, + "captures": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/captures", + "related": "/api/orders/PlalhWGrAb/captures" + } + }, + "voids": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/voids", + "related": "/api/orders/PlalhWGrAb/voids" + } + }, + "refunds": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/refunds", + "related": "/api/orders/PlalhWGrAb/refunds" + } + }, + "returns": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/returns", + "related": "/api/orders/PlalhWGrAb/returns" + } + }, + "order_subscriptions": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/order_subscriptions", + "related": "/api/orders/PlalhWGrAb/order_subscriptions" + } + }, + "order_factories": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/order_factories", + "related": "/api/orders/PlalhWGrAb/order_factories" + } + }, + "order_copies": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/order_copies", + "related": "/api/orders/PlalhWGrAb/order_copies" + } + }, + "recurring_order_copies": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/recurring_order_copies", + "related": "/api/orders/PlalhWGrAb/recurring_order_copies" + } + }, + "attachments": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/attachments", + "related": "/api/orders/PlalhWGrAb/attachments" + } + }, + "events": { + "links": { + "self": "/api/orders/PlalhWGrAb/relationships/events", + "related": "/api/orders/PlalhWGrAb/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6IjI2Y2Y0NjM0MmZmYzNhZGQiLCJ0ciI6IjEzYTQyZmQ2ZjQzMjI4N2YzNWM5YjRhYjIyMmE1YTM4Iiwic2EiOmZhbHNlLCJwciI6MC4wOTgwNDYsInRpIjoxNjc5MDQ4ODI5ODUxLCJpZCI6ImJlY2UyY2VmNmUwNWE4NTIifX0=", + "tracestate": "2097276@nr=0-0-2097276-83882605-bece2cef6e05a852-26cf46342ffc3add-0-0.098046-1679048829851", + "x-commercelayer-topic": "orders.create" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "commercelayer", + "providerLabel": "Commerce Layer", + "eventType": "percentage_discount_promotions.create", + "name": "percentage_discount_promotions.create", + "body": { + "data": { + "id": "lAvdbfPPvO", + "type": "percentage_discount_promotions", + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO" + }, + "attributes": { + "name": "Discounted", + "currency_code": "USD", + "starts_at": "2023-03-17T10:42:32.000Z", + "expires_at": "2023-03-21T00:00:00.000Z", + "total_usage_limit": 3, + "total_usage_count": 0, + "active": true, + "created_at": "2023-03-17T10:43:23.551Z", + "updated_at": "2023-03-17T10:43:23.551Z", + "reference": "Merry Christmas", + "reference_origin": "gift", + "metadata": {}, + "percentage": "4" + }, + "relationships": { + "market": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/market", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/market" + } + }, + "promotion_rules": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/promotion_rules", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/promotion_rules" + } + }, + "order_amount_promotion_rule": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/order_amount_promotion_rule", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/order_amount_promotion_rule" + } + }, + "sku_list_promotion_rule": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/sku_list_promotion_rule", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/sku_list_promotion_rule" + } + }, + "coupon_codes_promotion_rule": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/coupon_codes_promotion_rule", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/coupon_codes_promotion_rule" + } + }, + "attachments": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/attachments", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/attachments" + } + }, + "sku_list": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/sku_list", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/sku_list" + } + }, + "skus": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/skus", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/skus" + } + }, + "events": { + "links": { + "self": "/api/percentage_discount_promotions/lAvdbfPPvO/relationships/events", + "related": "/api/percentage_discount_promotions/lAvdbfPPvO/events" + } + } + }, + "meta": { + "mode": "test", + "organization_id": "jXdWbFWAYy" + } + } + }, + "extraHeaders": { + "newrelic": "eyJ2IjpbMCwxXSwiZCI6eyJ0eSI6IkFwcCIsImFjIjoiMjA5NzI3NiIsImFwIjoiODM4ODI2MDUiLCJ0eCI6IjVmMDMxNGE0OTYwMTRkOWUiLCJ0ciI6ImJkZGZhOWQ5NGUxYmVjZWY4OTc3NTk2OWIyNTcxN2EwIiwic2EiOmZhbHNlLCJwciI6MC4xNTQ5NjMsInRpIjoxNjc5MDQ5ODAzNTc5LCJpZCI6Ijk0NTJkMmM2YWM1NzdkYTAifX0=", + "tracestate": "2097276@nr=0-0-2097276-83882605-9452d2c6ac577da0-5f0314a496014d9e-0-0.154963-1679049803579", + "x-commercelayer-topic": "percentage_discount_promotions.create" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:commercelayer/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "create", + "name": "create", + "body": { + "ref": "demo", + "ref_type": "tag", + "master_branch": "main", + "description": null, + "pusher_type": "user", + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T23:04:11Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "create" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "delete", + "name": "delete", + "body": { + "ref": "hookdeckwrite-patch-1", + "ref_type": "branch", + "pusher_type": "user", + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T22:59:39Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "delete" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "issue_comment", + "name": "issue_comment", + "body": { + "action": "created", + "issue": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1", + "repository_url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/comments", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/events", + "html_url": "https://github.com/hookdeckwrite/simple-app/pull/1", + "id": 1610402038, + "node_id": "PR_kwDOI0v4Q85LUJ3o", + "number": 1, + "title": "Update README.md", + "user": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "labels": [], + "state": "closed", + "locked": false, + "assignee": null, + "assignees": [], + "milestone": null, + "comments": 2, + "created_at": "2023-03-05T22:57:12Z", + "updated_at": "2023-03-05T22:59:02Z", + "closed_at": "2023-03-05T22:58:17Z", + "author_association": "OWNER", + "active_lock_reason": null, + "draft": false, + "pull_request": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1", + "html_url": "https://github.com/hookdeckwrite/simple-app/pull/1", + "diff_url": "https://github.com/hookdeckwrite/simple-app/pull/1.diff", + "patch_url": "https://github.com/hookdeckwrite/simple-app/pull/1.patch", + "merged_at": "2023-03-05T22:58:17Z" + }, + "body": "Edited", + "reactions": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/timeline", + "performed_via_github_app": null, + "state_reason": null + }, + "comment": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments/1455234988", + "html_url": "https://github.com/hookdeckwrite/simple-app/pull/1#issuecomment-1455234988", + "issue_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1", + "id": 1455234988, + "node_id": "IC_kwDOI0v4Q85WvR-s", + "user": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2023-03-05T22:59:02Z", + "updated_at": "2023-03-05T22:59:02Z", + "author_association": "OWNER", + "body": "> Merged?\r\n\r\nYes", + "reactions": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments/1455234988/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "performed_via_github_app": null + }, + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T22:58:17Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "issue_comment" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "label", + "name": "label", + "body": { + "action": "deleted", + "label": { + "id": 5068260474, + "node_id": "LA_kwDOI0v4Q88AAAABLheEeg", + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels/enhancement", + "name": "enhancement", + "color": "a2eeef", + "default": true, + "description": "New feature or request" + }, + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T22:59:49Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "label" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "ping", + "name": "ping", + "body": { + "zen": "Speak like a human.", + "hook_id": 403738602, + "hook": { + "type": "Repository", + "id": 403738602, + "name": "web", + "active": true, + "events": [ + "*" + ], + "config": { + "content_type": "json", + "insecure_ssl": "0", + "secret": "********", + "url": "https://events.hookdeck.com/e/src_oRc6ItAA6fEu/github/2022-11-28" + }, + "updated_at": "2023-03-05T22:54:35Z", + "created_at": "2023-03-05T22:54:35Z", + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks/403738602", + "test_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks/403738602/test", + "ping_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks/403738602/pings", + "deliveries_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks/403738602/deliveries", + "last_response": { + "code": null, + "status": "unused", + "message": null + } + }, + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-01-23T06:50:15Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "ping" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "pull_request", + "name": "pull_request", + "body": { + "action": "labeled", + "number": 1, + "pull_request": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1", + "id": 1263574504, + "node_id": "PR_kwDOI0v4Q85LUJ3o", + "html_url": "https://github.com/hookdeckwrite/simple-app/pull/1", + "diff_url": "https://github.com/hookdeckwrite/simple-app/pull/1.diff", + "patch_url": "https://github.com/hookdeckwrite/simple-app/pull/1.patch", + "issue_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1", + "number": 1, + "state": "closed", + "locked": false, + "title": "Update README.md", + "user": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "body": "Edited", + "created_at": "2023-03-05T22:57:12Z", + "updated_at": "2023-03-05T23:01:24Z", + "closed_at": "2023-03-05T22:58:17Z", + "merged_at": "2023-03-05T22:58:17Z", + "merge_commit_sha": "b03b17320c22d5e46c5e302a150fdbe3abe39735", + "assignee": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "assignees": [ + { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + ], + "requested_reviewers": [], + "requested_teams": [], + "labels": [ + { + "id": 5068260465, + "node_id": "LA_kwDOI0v4Q88AAAABLheEcQ", + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels/documentation", + "name": "documentation", + "color": "0075ca", + "default": true, + "description": "Improvements or additions to documentation" + } + ], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1/commits", + "review_comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1/comments", + "review_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/comments", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/bc6a8bcfac73fae64f541c8ce1d405adbe04d920", + "head": { + "label": "hookdeckwrite:hookdeckwrite-patch-1", + "ref": "hookdeckwrite-patch-1", + "sha": "bc6a8bcfac73fae64f541c8ce1d405adbe04d920", + "user": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T22:59:49Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "base": { + "label": "hookdeckwrite:main", + "ref": "main", + "sha": "ec65202d8919f48daaaa2f5276311de544a0916e", + "user": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T22:59:49Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "allow_auto_merge": false, + "delete_branch_on_merge": false, + "allow_update_branch": false, + "use_squash_pr_title_as_default": false, + "squash_merge_commit_message": "COMMIT_MESSAGES", + "squash_merge_commit_title": "COMMIT_OR_PR_TITLE", + "merge_commit_message": "PR_TITLE", + "merge_commit_title": "MERGE_MESSAGE" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1" + }, + "html": { + "href": "https://github.com/hookdeckwrite/simple-app/pull/1" + }, + "issue": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1" + }, + "comments": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/1/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls/1/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/bc6a8bcfac73fae64f541c8ce1d405adbe04d920" + } + }, + "author_association": "OWNER", + "auto_merge": null, + "active_lock_reason": null, + "merged": true, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "comments": 2, + "review_comments": 0, + "maintainer_can_modify": false, + "commits": 1, + "additions": 1, + "deletions": 0, + "changed_files": 1 + }, + "label": { + "id": 5068260465, + "node_id": "LA_kwDOI0v4Q88AAAABLheEcQ", + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels/documentation", + "name": "documentation", + "color": "0075ca", + "default": true, + "description": "Improvements or additions to documentation" + }, + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T22:59:49Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "pull_request" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "push", + "name": "push", + "body": { + "ref": "refs/tags/demo", + "before": "0000000000000000000000000000000000000000", + "after": "b03b17320c22d5e46c5e302a150fdbe3abe39735", + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "name": "hookdeckwrite", + "email": "123277766+hookdeckwrite@users.noreply.github.com", + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://github.com/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": 1674453551, + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": 1678057451, + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main", + "stargazers": 0, + "master_branch": "main" + }, + "pusher": { + "name": "hookdeckwrite", + "email": "123277766+hookdeckwrite@users.noreply.github.com" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "created": true, + "deleted": false, + "forced": false, + "base_ref": "refs/heads/main", + "compare": "https://github.com/hookdeckwrite/simple-app/compare/demo", + "commits": [], + "head_commit": { + "id": "b03b17320c22d5e46c5e302a150fdbe3abe39735", + "tree_id": "3b0153ccb69afba6845b1c4b76cbf8f4b465b392", + "distinct": true, + "message": "Merge pull request #1 from hookdeckwrite/hookdeckwrite-patch-1\n\nUpdate README.md", + "timestamp": "2023-03-05T23:58:17+01:00", + "url": "https://github.com/hookdeckwrite/simple-app/commit/b03b17320c22d5e46c5e302a150fdbe3abe39735", + "author": { + "name": "hookdeckwrite", + "email": "123277766+hookdeckwrite@users.noreply.github.com", + "username": "hookdeckwrite" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com", + "username": "web-flow" + }, + "added": [], + "removed": [], + "modified": [ + "README.md" + ] + } + }, + "extraHeaders": { + "x-github-event": "push" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "release", + "name": "release", + "body": { + "action": "released", + "release": { + "url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases/94570490", + "assets_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases/94570490/assets", + "upload_url": "https://uploads.github.com/repos/hookdeckwrite/simple-app/releases/94570490/assets{?name,label}", + "html_url": "https://github.com/hookdeckwrite/simple-app/releases/tag/demo", + "id": 94570490, + "author": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "node_id": "RE_kwDOI0v4Q84Fowf6", + "tag_name": "demo", + "target_commitish": "main", + "name": "demo-release", + "draft": false, + "prerelease": false, + "created_at": "2023-03-05T22:58:17Z", + "published_at": "2023-03-05T23:04:11Z", + "assets": [], + "tarball_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tarball/demo", + "zipball_url": "https://api.github.com/repos/hookdeckwrite/simple-app/zipball/demo", + "body": "See demo release" + }, + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-01-23T05:59:11Z", + "pushed_at": "2023-03-05T23:04:11Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 1, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "release" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "star", + "name": "star", + "body": { + "action": "created", + "starred_at": "2023-03-06T04:19:17Z", + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-03-06T04:19:17Z", + "pushed_at": "2023-03-05T23:04:11Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 3, + "stargazers_count": 1, + "watchers_count": 1, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 1, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "star" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "github", + "providerLabel": "GitHub", + "presetId": "github", + "eventType": "watch", + "name": "watch", + "body": { + "action": "started", + "repository": { + "id": 592181315, + "node_id": "R_kgDOI0v4Qw", + "name": "simple-app", + "full_name": "hookdeckwrite/simple-app", + "private": false, + "owner": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/hookdeckwrite/simple-app", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/hookdeckwrite/simple-app", + "forks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/forks", + "keys_url": "https://api.github.com/repos/hookdeckwrite/simple-app/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/hookdeckwrite/simple-app/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/hookdeckwrite/simple-app/teams", + "hooks_url": "https://api.github.com/repos/hookdeckwrite/simple-app/hooks", + "issue_events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/events{/number}", + "events_url": "https://api.github.com/repos/hookdeckwrite/simple-app/events", + "assignees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/assignees{/user}", + "branches_url": "https://api.github.com/repos/hookdeckwrite/simple-app/branches{/branch}", + "tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/tags", + "blobs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/hookdeckwrite/simple-app/statuses/{sha}", + "languages_url": "https://api.github.com/repos/hookdeckwrite/simple-app/languages", + "stargazers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/stargazers", + "contributors_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contributors", + "subscribers_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscribers", + "subscription_url": "https://api.github.com/repos/hookdeckwrite/simple-app/subscription", + "commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/hookdeckwrite/simple-app/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/hookdeckwrite/simple-app/contents/{+path}", + "compare_url": "https://api.github.com/repos/hookdeckwrite/simple-app/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/hookdeckwrite/simple-app/merges", + "archive_url": "https://api.github.com/repos/hookdeckwrite/simple-app/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/hookdeckwrite/simple-app/downloads", + "issues_url": "https://api.github.com/repos/hookdeckwrite/simple-app/issues{/number}", + "pulls_url": "https://api.github.com/repos/hookdeckwrite/simple-app/pulls{/number}", + "milestones_url": "https://api.github.com/repos/hookdeckwrite/simple-app/milestones{/number}", + "notifications_url": "https://api.github.com/repos/hookdeckwrite/simple-app/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/hookdeckwrite/simple-app/labels{/name}", + "releases_url": "https://api.github.com/repos/hookdeckwrite/simple-app/releases{/id}", + "deployments_url": "https://api.github.com/repos/hookdeckwrite/simple-app/deployments", + "created_at": "2023-01-23T05:59:11Z", + "updated_at": "2023-03-06T04:19:17Z", + "pushed_at": "2023-03-05T23:04:11Z", + "git_url": "git://github.com/hookdeckwrite/simple-app.git", + "ssh_url": "git@github.com:hookdeckwrite/simple-app.git", + "clone_url": "https://github.com/hookdeckwrite/simple-app.git", + "svn_url": "https://github.com/hookdeckwrite/simple-app", + "homepage": null, + "size": 3, + "stargazers_count": 1, + "watchers_count": 1, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "has_discussions": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 0, + "open_issues": 0, + "watchers": 1, + "default_branch": "main" + }, + "sender": { + "login": "hookdeckwrite", + "id": 123277766, + "node_id": "U_kgDOB1kRxg", + "avatar_url": "https://avatars.githubusercontent.com/u/123277766?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/hookdeckwrite", + "html_url": "https://github.com/hookdeckwrite", + "followers_url": "https://api.github.com/users/hookdeckwrite/followers", + "following_url": "https://api.github.com/users/hookdeckwrite/following{/other_user}", + "gists_url": "https://api.github.com/users/hookdeckwrite/gists{/gist_id}", + "starred_url": "https://api.github.com/users/hookdeckwrite/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/hookdeckwrite/subscriptions", + "organizations_url": "https://api.github.com/users/hookdeckwrite/orgs", + "repos_url": "https://api.github.com/users/hookdeckwrite/repos", + "events_url": "https://api.github.com/users/hookdeckwrite/events{/privacy}", + "received_events_url": "https://api.github.com/users/hookdeckwrite/received_events", + "type": "User", + "site_admin": false + } + }, + "extraHeaders": { + "x-github-event": "watch" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:github/2022-11-28", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Confidential Issue Hook", + "name": "Confidential Issue Hook", + "body": { + "object_kind": "issue", + "event_type": "issue", + "user": { + "id": 13576927, + "name": "Hookdeck Write", + "username": "hookdeckwrite", + "avatar_url": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?s=80&d=identicon", + "email": "[REDACTED]" + }, + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "object_attributes": { + "author_id": 13576927, + "closed_at": null, + "confidential": false, + "created_at": "2023-02-20T19:43:43.306Z", + "description": "", + "discussion_locked": null, + "due_date": null, + "id": 124017598, + "iid": 1, + "last_edited_at": null, + "last_edited_by_id": null, + "milestone_id": null, + "moved_to_id": null, + "duplicated_to_id": null, + "project_id": 42958108, + "relative_position": 513, + "state_id": 1, + "time_estimate": 0, + "title": "test tissue", + "updated_at": "2023-02-20T19:43:43.306Z", + "updated_by_id": null, + "weight": null, + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/issues/1", + "total_time_spent": 0, + "time_change": 0, + "human_total_time_spent": null, + "human_time_change": null, + "human_time_estimate": null, + "assignee_ids": [], + "assignee_id": null, + "labels": [], + "state": "opened", + "severity": "unknown" + }, + "labels": [], + "changes": {}, + "repository": { + "name": "Hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "description": null, + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write" + } + }, + "extraHeaders": { + "x-gitlab-event": "Confidential Issue Hook", + "x-gitlab-event-uuid": "f92eb1b5-1a40-4e58-b001-54d2a86fed80", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Issue Hook", + "name": "Issue Hook", + "body": { + "object_kind": "issue", + "event_type": "issue", + "user": { + "id": 13576927, + "name": "Hookdeck Write", + "username": "hookdeckwrite", + "avatar_url": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?s=80&d=identicon", + "email": "[REDACTED]" + }, + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "object_attributes": { + "author_id": 13576927, + "closed_at": null, + "confidential": false, + "created_at": "2023-02-20T19:43:43.306Z", + "description": "", + "discussion_locked": null, + "due_date": null, + "id": 124017598, + "iid": 1, + "last_edited_at": null, + "last_edited_by_id": null, + "milestone_id": null, + "moved_to_id": null, + "duplicated_to_id": null, + "project_id": 42958108, + "relative_position": 513, + "state_id": 1, + "time_estimate": 0, + "title": "test tissue", + "updated_at": "2023-02-20T19:43:43.306Z", + "updated_by_id": null, + "weight": null, + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/issues/1", + "total_time_spent": 0, + "time_change": 0, + "human_total_time_spent": null, + "human_time_change": null, + "human_time_estimate": null, + "assignee_ids": [], + "assignee_id": null, + "labels": [], + "state": "opened", + "severity": "unknown" + }, + "labels": [], + "changes": {}, + "repository": { + "name": "Hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "description": null, + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write" + } + }, + "extraHeaders": { + "x-gitlab-event": "Issue Hook", + "x-gitlab-event-uuid": "3e241fee-25e7-48ae-9d58-a1c88528f92f", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Note Hook", + "name": "Note Hook", + "body": { + "object_kind": "note", + "event_type": "note", + "user": { + "id": 13576927, + "name": "Hookdeck Write", + "username": "hookdeckwrite", + "avatar_url": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?s=80&d=identicon", + "email": "[REDACTED]" + }, + "project_id": 42958108, + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "object_attributes": { + "attachment": null, + "author_id": 13576927, + "change_position": { + "base_sha": null, + "start_sha": null, + "head_sha": null, + "old_path": null, + "new_path": null, + "position_type": "text", + "old_line": null, + "new_line": null, + "line_range": null + }, + "commit_id": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "created_at": "2023-03-06 04:38:16 UTC", + "discussion_id": "1f3a53063b9739107c9dc86a12aebb03d197a91b", + "id": 1301562881, + "line_code": "8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_11_12", + "note": "commented", + "noteable_id": null, + "noteable_type": "Commit", + "original_position": { + "base_sha": "c32d8669f6b1afa6c8b4b634fad5201f6dd9d2f7", + "start_sha": "c32d8669f6b1afa6c8b4b634fad5201f6dd9d2f7", + "head_sha": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "old_path": "README.md", + "new_path": "README.md", + "position_type": "text", + "old_line": null, + "new_line": 12, + "line_range": null + }, + "position": { + "base_sha": "c32d8669f6b1afa6c8b4b634fad5201f6dd9d2f7", + "start_sha": "c32d8669f6b1afa6c8b4b634fad5201f6dd9d2f7", + "head_sha": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "old_path": "README.md", + "new_path": "README.md", + "position_type": "text", + "old_line": null, + "new_line": 12, + "line_range": null + }, + "project_id": 42958108, + "resolved_at": null, + "resolved_by_id": null, + "resolved_by_push": null, + "st_diff": null, + "system": false, + "type": "DiffNote", + "updated_at": "2023-03-06 04:38:16 UTC", + "updated_by_id": null, + "description": "commented", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/commit/6752dd8e9b70a034e36b495d26626dafef42c53c#note_1301562881" + }, + "repository": { + "name": "Hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "description": null, + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write" + }, + "commit": { + "id": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "message": "Update README.md", + "title": "Update README.md", + "timestamp": "2023-01-27T15:11:04+00:00", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/commit/6752dd8e9b70a034e36b495d26626dafef42c53c", + "author": { + "name": "Hookdeck Write", + "email": "hookdeckwrite@gmail.com" + } + } + }, + "extraHeaders": { + "x-gitlab-event": "Note Hook", + "x-gitlab-event-uuid": "29f888a0-b898-487c-a0bf-ce135adfa206", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Push Hook", + "name": "Push Hook", + "body": { + "object_kind": "push", + "event_name": "push", + "before": "0000000000000000000000000000000000000000", + "after": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "ref": "refs/heads/manage", + "checkout_sha": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "message": null, + "user_id": 13576927, + "user_name": "Hookdeck Write", + "user_username": "hookdeckwrite", + "user_email": null, + "user_avatar": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?s=80&d=identicon", + "project_id": 42958108, + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "commits": [], + "total_commits_count": 0, + "push_options": {}, + "repository": { + "name": "Hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "description": null, + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "visibility_level": 0 + } + }, + "extraHeaders": { + "x-gitlab-event": "Push Hook", + "x-gitlab-event-uuid": "5866755a-6a7d-430f-b7c3-bd29ec77e3f3", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Release Hook", + "name": "Release Hook", + "body": { + "id": 7054492, + "created_at": "2023-03-06 04:44:05 UTC", + "description": "Leta see", + "name": "os", + "released_at": "2023-03-06 04:44:05 UTC", + "tag": "observe", + "object_kind": "release", + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/releases/observe", + "action": "create", + "assets": { + "count": 4, + "links": [], + "sources": [ + { + "format": "zip", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/archive/observe/hookdeck-write-observe.zip" + }, + { + "format": "tar.gz", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/archive/observe/hookdeck-write-observe.tar.gz" + }, + { + "format": "tar.bz2", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/archive/observe/hookdeck-write-observe.tar.bz2" + }, + { + "format": "tar", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/archive/observe/hookdeck-write-observe.tar" + } + ] + }, + "commit": { + "id": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "message": "Update README.md", + "title": "Update README.md", + "timestamp": "2023-01-27T15:11:04+00:00", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/commit/6752dd8e9b70a034e36b495d26626dafef42c53c", + "author": { + "name": "Hookdeck Write", + "email": "hookdeckwrite@gmail.com" + } + } + }, + "extraHeaders": { + "x-gitlab-event": "Release Hook", + "x-gitlab-event-uuid": "5977e99f-9dd2-41b3-b169-1e66461f3995", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Tag Push Hook", + "name": "Tag Push Hook", + "body": { + "object_kind": "tag_push", + "event_name": "tag_push", + "before": "0000000000000000000000000000000000000000", + "after": "844cdc62966d4bf4b660285912fa530bf9c63257", + "ref": "refs/tags/observe", + "checkout_sha": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "message": "observing release", + "user_id": 13576927, + "user_name": "Hookdeck Write", + "user_username": "hookdeckwrite", + "user_email": null, + "user_avatar": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?s=80&d=identicon", + "project_id": 42958108, + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "commits": [ + { + "id": "6752dd8e9b70a034e36b495d26626dafef42c53c", + "message": "Update README.md", + "title": "Update README.md", + "timestamp": "2023-01-27T15:11:04+00:00", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/commit/6752dd8e9b70a034e36b495d26626dafef42c53c", + "author": { + "name": "Hookdeck Write", + "email": "hookdeckwrite@gmail.com" + }, + "added": [], + "modified": [ + "README.md" + ], + "removed": [] + } + ], + "total_commits_count": 1, + "push_options": {}, + "repository": { + "name": "Hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "description": null, + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "visibility_level": 0 + } + }, + "extraHeaders": { + "x-gitlab-event": "Tag Push Hook", + "x-gitlab-event-uuid": "77d0cb42-4aba-4aed-9c66-79e65c979e71", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "gitlab", + "providerLabel": "GitLab", + "eventType": "Wiki Page Hook", + "name": "Wiki Page Hook", + "body": { + "object_kind": "wiki_page", + "user": { + "id": 13576927, + "name": "Hookdeck Write", + "username": "hookdeckwrite", + "avatar_url": "https://secure.gravatar.com/avatar/8d9e5d44f7eb9d70f015e80e50d1fa98?s=80&d=identicon", + "email": "[REDACTED]" + }, + "project": { + "id": 42958108, + "name": "Hookdeck-write", + "description": null, + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "avatar_url": null, + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git", + "namespace": "Hookdeck Write", + "visibility_level": 0, + "path_with_namespace": "hookdeckwrite/hookdeck-write", + "default_branch": "main", + "ci_config_path": "", + "homepage": "https://gitlab.com/hookdeckwrite/hookdeck-write", + "url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.git", + "http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.git" + }, + "wiki": { + "web_url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/wikis/home", + "git_ssh_url": "git@gitlab.com:hookdeckwrite/hookdeck-write.wiki.git", + "git_http_url": "https://gitlab.com/hookdeckwrite/hookdeck-write.wiki.git", + "path_with_namespace": "hookdeckwrite/hookdeck-write.wiki", + "default_branch": "main" + }, + "object_attributes": { + "content": "Hello new wiki page", + "format": "markdown", + "message": "Create home", + "title": "home", + "slug": "home", + "url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/wikis/home", + "action": "create", + "diff_url": "https://gitlab.com/hookdeckwrite/hookdeck-write/-/wikis/home/diff?version_id=fa0715e03fc718299e66127d92d2d0eebeacc69d" + } + }, + "extraHeaders": { + "x-gitlab-event": "Wiki Page Hook", + "x-gitlab-event-uuid": "f697fa74-d5e3-45dc-8be2-5418ea11362b", + "x-gitlab-instance": "https://gitlab.com", + "x-gitlab-token": "secrettoken" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:gitlab/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "hubspot", + "providerLabel": "HubSpot", + "eventType": "COMPANY", + "name": "COMPANY", + "body": { + "portalId": 27145807, + "objectType": "COMPANY", + "objectTypeId": "0-2", + "objectId": 7298894071, + "properties": { + "country": { + "versions": [ + { + "name": "country", + "value": "United States", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "United States", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "city": { + "versions": [ + { + "name": "city", + "value": "Mountain View", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "Mountain View", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "num_associated_contacts": { + "versions": [ + { + "name": "num_associated_contacts", + "value": "0", + "timestamp": 1680448390046, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680448390046, + "persistenceTimestamp": 1680448390046, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "timezone": { + "versions": [ + { + "name": "timezone", + "value": "America/Los_Angeles", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "America/Los_Angeles", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "facebook_company_page": { + "versions": [ + { + "name": "facebook_company_page", + "value": "https://facebook.com/google", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "https://facebook.com/google", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "hs_pipeline": { + "versions": [ + { + "name": "hs_pipeline", + "value": "companies-lifecycle-pipeline", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "companies-lifecycle-pipeline", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "createdate": { + "versions": [ + { + "name": "createdate", + "value": "1680448389197", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680448389197", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "description": { + "versions": [ + { + "name": "description", + "value": "Google LLC is an American multinational technology company that focuses on artificial intelligence, search engine, online advertising, cloud computing, computer software, quantum computing, e-commerce, and consumer electronics.", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "Google LLC is an American multinational technology company that focuses on artificial intelligence, search engine, online advertising, cloud computing, computer software, quantum computing, e-commerce, and consumer electronics.", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_num_blockers": { + "versions": [ + { + "name": "hs_num_blockers", + "value": "0", + "timestamp": 1680448390047, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680448390047, + "persistenceTimestamp": 1680448390047, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "industry": { + "versions": [ + { + "name": "industry", + "value": "COMPUTER_SOFTWARE", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "COMPUTER_SOFTWARE", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "total_money_raised": { + "versions": [ + { + "name": "total_money_raised", + "value": "786.1M", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "786.1M", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "web_technologies": { + "versions": [ + { + "name": "web_technologies", + "value": "amazon_s3;wordpress;google_analytics;amazon__cloudfront;double_click;google_tag_manager", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "amazon_s3;wordpress;google_analytics;amazon__cloudfront;double_click;google_tag_manager", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "numberofemployees": { + "versions": [ + { + "name": "numberofemployees", + "value": "10000", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "10000", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "linkedin_company_page": { + "versions": [ + { + "name": "linkedin_company_page", + "value": "https://www.linkedin.com/company/google", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "https://www.linkedin.com/company/google", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hubspot_owner_id": { + "versions": [ + { + "name": "hubspot_owner_id", + "value": "823493108", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "823493108", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "annualrevenue": { + "versions": [ + { + "name": "annualrevenue", + "value": "1000000000", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1000000000", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_created_by_user_id": { + "versions": [ + { + "name": "hs_created_by_user_id", + "value": "50549138", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50549138", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "founded_year": { + "versions": [ + { + "name": "founded_year", + "value": "1998", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1998", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "state": { + "versions": [ + { + "name": "state", + "value": "CA", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "CA", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_all_owner_ids": { + "versions": [ + { + "name": "hs_all_owner_ids", + "value": "823493108", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "823493108", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "linkedinbio": { + "versions": [ + { + "name": "linkedinbio", + "value": "Google LLC is an American multinational technology company that focuses on artificial intelligence, search engine, online advertising, cloud computing, computer software, quantum computing, e-commerce, and consumer electronics.", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "Google LLC is an American multinational technology company that focuses on artificial intelligence, search engine, online advertising, cloud computing, computer software, quantum computing, e-commerce, and consumer electronics.", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "hs_num_open_deals": { + "versions": [ + { + "name": "hs_num_open_deals", + "value": "0", + "timestamp": 1680448390046, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680448390046, + "persistenceTimestamp": 1680448390046, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "zip": { + "versions": [ + { + "name": "zip", + "value": "94043", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "94043", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "website": { + "versions": [ + { + "name": "website", + "value": "google.com", + "timestamp": 1680448389197, + "source": "CALCULATED", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "google.com", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CALCULATED", + "sourceId": null, + "updatedByUserId": 50549138 + }, + "address": { + "versions": [ + { + "name": "address", + "value": "1600 Amphitheatre Parkway", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1600 Amphitheatre Parkway", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "hs_user_ids_of_all_owners": { + "versions": [ + { + "name": "hs_user_ids_of_all_owners", + "value": "50549138", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50549138", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "twitterhandle": { + "versions": [ + { + "name": "twitterhandle", + "value": "google", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "google", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "hs_target_account_probability": { + "versions": [ + { + "name": "hs_target_account_probability", + "value": "0.49565839767456055", + "timestamp": 1680448390378, + "source": "SALES", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0.49565839767456055", + "timestamp": 1680448390378, + "persistenceTimestamp": 1680448390378, + "source": "SALES", + "sourceId": null, + "updatedByUserId": null + }, + "hs_lastmodifieddate": { + "versions": [ + { + "name": "hs_lastmodifieddate", + "value": "1680448390378", + "timestamp": 1680448390378, + "source": "SALES", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680448390378", + "timestamp": 1680448390378, + "persistenceTimestamp": 1680448390378, + "source": "SALES", + "sourceId": null, + "updatedByUserId": null + }, + "hs_num_decision_makers": { + "versions": [ + { + "name": "hs_num_decision_makers", + "value": "0", + "timestamp": 1680448390046, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680448390046, + "persistenceTimestamp": 1680448390046, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hubspot_owner_assigneddate": { + "versions": [ + { + "name": "hubspot_owner_assigneddate", + "value": "1680448389197", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680448389197", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "phone": { + "versions": [ + { + "name": "phone", + "value": "+1 650-253-0000", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "+1 650-253-0000", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "domain": { + "versions": [ + { + "name": "domain", + "value": "google.com", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "google.com", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_num_child_companies": { + "versions": [ + { + "name": "hs_num_child_companies", + "value": "0", + "timestamp": 1680448390047, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680448390047, + "persistenceTimestamp": 1680448390047, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hs_num_contacts_with_buying_roles": { + "versions": [ + { + "name": "hs_num_contacts_with_buying_roles", + "value": "0", + "timestamp": 1680448390046, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680448390046, + "persistenceTimestamp": 1680448390046, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hs_object_id": { + "versions": [ + { + "name": "hs_object_id", + "value": "7298894071", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "7298894071", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "is_public": { + "versions": [ + { + "name": "is_public", + "value": "true", + "timestamp": 1680448389477, + "sourceId": "CompanyInsightsPropertyMappings", + "source": "COMPANY_INSIGHTS", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "true", + "timestamp": 1680448389477, + "persistenceTimestamp": 1680448389477, + "source": "COMPANY_INSIGHTS", + "sourceId": "CompanyInsightsPropertyMappings", + "updatedByUserId": null + }, + "name": { + "versions": [ + { + "name": "name", + "value": "Google", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "Google", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "lifecyclestage": { + "versions": [ + { + "name": "lifecyclestage", + "value": "lead", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "lead", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_updated_by_user_id": { + "versions": [ + { + "name": "hs_updated_by_user_id", + "value": "50549138", + "timestamp": 1680448389197, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "bed919cd-4645-438c-bf3d-a998796ec860", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50549138", + "timestamp": 1680448389197, + "persistenceTimestamp": 1680448389197, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + } + }, + "version": 4, + "secondaryIdentifier": "google.com", + "isDeleted": false + }, + "extraHeaders": { + "x-hubspot-client-ip": "172.21.85.188", + "x-hubspot-correlation-id": "4c1e9d73-2536-463b-b98c-ccb602123544", + "x-hubspot-request-timestamp": "1680448400841", + "x-hubspot-requesting-chain-bin": "CkwKQEF1dG9tYXRpb25QbGF0Zm9ybUV4ZWN1dGlvbi1leGVjdXRpb25TbG93QWN0aW9uVHlwZUxhbmVXYWxXb3JrZXISCFBST0NGSUxFCjEKJUF1dG9tYXRpb25BY3Rpb25zRXhlY3V0aW9uU2VydmljZS13ZWISCFBST0NGSUxFEgIIAg==", + "x-hubspot-timeout-millis": "44986", + "x-trace": "2B77CBE81E60CC0E534F6EE10E6A6744D39CC6F13A000000000000000000" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:hubspot/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "hubspot", + "providerLabel": "HubSpot", + "eventType": "CONTACT", + "name": "CONTACT", + "body": { + "vid": 251, + "canonical-vid": 251, + "merged-vids": [], + "portal-id": 27145807, + "is-contact": true, + "properties": { + "hs_latest_source_data_2": { + "value": "userId:50549138", + "versions": [ + { + "value": "userId:50549138", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_is_unworked": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "CALCULATED", + "source-id": "CalculatedPropertyComputer", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_latest_source_data_1": { + "value": "CRM_UI", + "versions": [ + { + "value": "CRM_UI", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "associatedcompanyid": { + "value": "7298823368", + "versions": [ + { + "value": "7298823368", + "source-type": "CALCULATED", + "source-id": "RollupProperties", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100462, + "selected": false + } + ] + }, + "firstname": { + "value": "Kovat", + "versions": [ + { + "value": "Kovat", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_latest_source": { + "value": "OFFLINE", + "versions": [ + { + "value": "OFFLINE", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "num_unique_conversion_events": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "MERGE_CONTACTS", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100250, + "selected": false + } + ] + }, + "hs_analytics_revenue": { + "value": "0.0", + "versions": [ + { + "value": "0.0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_pipeline": { + "value": "contacts-lifecycle-pipeline", + "versions": [ + { + "value": "contacts-lifecycle-pipeline", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "createdate": { + "value": "1680448099688", + "versions": [ + { + "value": "1680448099688", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_social_num_broadcast_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_analytics_num_visits": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_sequences_actively_enrolled_count": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "CALCULATED", + "source-id": "RollupProperties", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100462, + "selected": false + } + ] + }, + "hs_social_linkedin_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hubspot_owner_id": { + "value": "823493108", + "versions": [ + { + "value": "823493108", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_analytics_source": { + "value": "OFFLINE", + "versions": [ + { + "value": "OFFLINE", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_created_by_user_id": { + "value": "50549138", + "versions": [ + { + "value": "50549138", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_analytics_num_page_views": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_email_domain": { + "value": "cyclesat.com", + "versions": [ + { + "value": "cyclesat.com", + "source-type": "CALCULATED", + "source-id": null, + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_all_owner_ids": { + "value": "823493108", + "versions": [ + { + "value": "823493108", + "source-type": "CALCULATED", + "source-id": "PermissionsUpdater", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100455, + "selected": false + } + ] + }, + "hs_count_is_worked": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "CALCULATED", + "source-id": "CalculatedPropertyComputer", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "email": { + "value": "kovatif642@cyclesat.com", + "versions": [ + { + "value": "kovatif642@cyclesat.com", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_latest_source_timestamp": { + "value": "1680448099780", + "versions": [ + { + "value": "1680448099780", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "jobtitle": { + "value": "Writer", + "versions": [ + { + "value": "Writer", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_analytics_first_timestamp": { + "value": "1680448099688", + "versions": [ + { + "value": "1680448099688", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "lastmodifieddate": { + "value": "1680448122728", + "versions": [ + { + "value": "1680448122728", + "source-type": "AUTOMATION_PLATFORM", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448122728, + "selected": false + }, + { + "value": "1680448110678", + "source-type": "API", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + }, + { + "value": "1680448100475", + "source-type": "API", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100475, + "selected": false + }, + { + "value": "1680448100462", + "source-type": "API", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100462, + "selected": false + }, + { + "value": "1680448099688", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_social_google_plus_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_user_ids_of_all_owners": { + "value": "50549138", + "versions": [ + { + "value": "50549138", + "source-type": "CALCULATED", + "source-id": "PermissionsUpdater", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100455, + "selected": false + } + ] + }, + "hs_analytics_average_page_views": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_all_contact_vids": { + "value": "251", + "versions": [ + { + "value": "251", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "lastname": { + "value": "Tif", + "versions": [ + { + "value": "Tif", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_social_facebook_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_is_contact": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "CALCULATED", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hubspot_owner_assigneddate": { + "value": "1680448099688", + "versions": [ + { + "value": "1680448099688", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "num_conversion_events": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "MERGE_CONTACTS", + "source-id": null, + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448100250, + "selected": false + } + ] + }, + "hs_object_id": { + "value": "251", + "versions": [ + { + "value": "251", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "currentlyinworkflow": { + "value": "true", + "versions": [ + { + "value": "true", + "source-type": "AUTOMATION_PLATFORM", + "source-id": "785646042", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448122489, + "selected": false + } + ] + }, + "hs_analytics_num_event_completions": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_analytics_source_data_2": { + "value": "userId:50549138", + "versions": [ + { + "value": "userId:50549138", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_social_twitter_clicks": { + "value": "0", + "versions": [ + { + "value": "0", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_analytics_source_data_1": { + "value": "CRM_UI", + "versions": [ + { + "value": "CRM_UI", + "source-type": "ANALYTICS", + "source-id": "ContactAnalyticsDetailsUpdateWorker", + "source-label": null, + "updated-by-user-id": null, + "timestamp": 1680448110678, + "selected": false + } + ] + }, + "hs_lifecyclestage_lead_date": { + "value": "1680448099688", + "versions": [ + { + "value": "1680448099688", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "lifecyclestage": { + "value": "lead", + "versions": [ + { + "value": "lead", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_count_is_unworked": { + "value": "1", + "versions": [ + { + "value": "1", + "source-type": "CALCULATED", + "source-id": "CalculatedPropertyComputer", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + }, + "hs_updated_by_user_id": { + "value": "50549138", + "versions": [ + { + "value": "50549138", + "source-type": "CRM_UI", + "source-id": "userId:50549138", + "source-label": null, + "updated-by-user-id": 50549138, + "timestamp": 1680448099688, + "selected": false + } + ] + } + }, + "form-submissions": [], + "list-memberships": [ + { + "static-list-id": 21, + "internal-list-id": 2147483643, + "timestamp": 1680448122038, + "vid": 251, + "is-member": true + } + ], + "identity-profiles": [ + { + "vid": 251, + "is-deleted": false, + "is-contact": false, + "pointer-vid": 0, + "previous-vid": 0, + "linked-vids": [], + "saved-at-timestamp": 0, + "deleted-changed-timestamp": 0, + "identities": [ + { + "type": "EMAIL", + "value": "kovatif642@cyclesat.com", + "timestamp": 1680448099688, + "is-primary": true, + "source": "UNSPECIFIED" + }, + { + "type": "LEAD_GUID", + "value": "06c11203-d92d-42d3-a81c-d2a75030b4be", + "timestamp": 1680448099774, + "source": "UNSPECIFIED" + } + ] + } + ], + "merge-audits": [], + "associated-company": { + "company-id": 7298823368, + "portal-id": 27145807, + "properties": { + "num_associated_contacts": { + "value": "1" + }, + "hs_pipeline": { + "value": "companies-lifecycle-pipeline" + }, + "hs_analytics_latest_source_data_2": { + "value": "userId:50549138" + }, + "createdate": { + "value": "1680448100221" + }, + "hs_num_blockers": { + "value": "0" + }, + "hs_analytics_latest_source_data_1": { + "value": "CRM_UI" + }, + "hs_analytics_num_visits": { + "value": "0" + }, + "hs_analytics_latest_source_timestamp": { + "value": "1680448099780" + }, + "hs_analytics_source": { + "value": "OFFLINE" + }, + "hs_analytics_num_page_views": { + "value": "0" + }, + "hs_num_open_deals": { + "value": "0" + }, + "website": { + "value": "cyclesat.com" + }, + "hs_analytics_first_timestamp": { + "value": "1680448099688" + }, + "first_contact_createdate": { + "value": "1680448099688" + }, + "hs_target_account_probability": { + "value": "0.38625118136405945" + }, + "hs_lastmodifieddate": { + "value": "1680448113387" + }, + "hs_num_decision_makers": { + "value": "0" + }, + "num_conversion_events": { + "value": "0" + }, + "domain": { + "value": "cyclesat.com" + }, + "name": { + "value": "Cyclesat" + }, + "hs_object_id": { + "value": "7298823368" + }, + "hs_num_contacts_with_buying_roles": { + "value": "0" + }, + "hs_num_child_companies": { + "value": "0" + }, + "hs_analytics_source_data_2": { + "value": "userId:50549138" + }, + "hs_analytics_source_data_1": { + "value": "CRM_UI" + }, + "hs_analytics_latest_source": { + "value": "OFFLINE" + }, + "lifecyclestage": { + "value": "lead" + } + } + }, + "associated-owner": { + "first-name": "Hookdeck", + "last-name": "Write", + "email": "hookdeckwrite@gmail.com", + "type": "PERSON", + "hubspot-user-id": 50549138 + } + }, + "extraHeaders": { + "x-hubspot-client-ip": "172.21.85.188", + "x-hubspot-correlation-id": "a14b39fd-3844-4247-9854-4fe205102f28", + "x-hubspot-request-timestamp": "1680448123263", + "x-hubspot-requesting-chain-bin": "CkwKQEF1dG9tYXRpb25QbGF0Zm9ybUV4ZWN1dGlvbi1leGVjdXRpb25TbG93QWN0aW9uVHlwZUxhbmVXYWxXb3JrZXISCFBST0NGSUxFCjEKJUF1dG9tYXRpb25BY3Rpb25zRXhlY3V0aW9uU2VydmljZS13ZWISCFBST0NGSUxFEgIIAg==", + "x-hubspot-timeout-millis": "44958", + "x-trace": "2B9340EAC52DB4308D3685ABA9D233DCB33D984B1C000000000000000000" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:hubspot/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "hubspot", + "providerLabel": "HubSpot", + "eventType": "DEAL", + "name": "DEAL", + "body": { + "portalId": 27145807, + "objectType": "DEAL", + "objectTypeId": "0-3", + "objectId": 7036839888, + "properties": { + "hs_closed_amount_in_home_currency": { + "versions": [ + { + "name": "hs_closed_amount_in_home_currency", + "value": "0", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "dealname": { + "versions": [ + { + "name": "dealname", + "value": "High Deal", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "High Deal", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_num_associated_deal_registrations": { + "versions": [ + { + "name": "hs_num_associated_deal_registrations", + "value": "0", + "timestamp": 1680442574707, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574707, + "persistenceTimestamp": 1680442574707, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "num_associated_contacts": { + "versions": [ + { + "name": "num_associated_contacts", + "value": "0", + "timestamp": 1680442574707, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574707, + "persistenceTimestamp": 1680442574707, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hs_forecast_amount": { + "versions": [ + { + "name": "hs_forecast_amount", + "value": "50000000", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50000000", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "createdate": { + "versions": [ + { + "name": "createdate", + "value": "1680442574451", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680442574451", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "amount_in_home_currency": { + "versions": [ + { + "name": "amount_in_home_currency", + "value": "50000000", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50000000", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "hs_is_closed": { + "versions": [ + { + "name": "hs_is_closed", + "value": "false", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "false", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "days_to_close": { + "versions": [ + { + "name": "days_to_close", + "value": "27", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "27", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "hs_deal_stage_probability": { + "versions": [ + { + "name": "hs_deal_stage_probability", + "value": "0.200000000000000011102230246251565404236316680908203125", + "timestamp": 1680442574957, + "sourceId": "DealStageProbabilityHandler", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0.200000000000000011102230246251565404236316680908203125", + "timestamp": 1680442574957, + "persistenceTimestamp": 1680442574957, + "source": "CALCULATED", + "sourceId": "DealStageProbabilityHandler", + "updatedByUserId": null + }, + "hs_deal_stage_probability_shadow": { + "versions": [ + { + "name": "hs_deal_stage_probability_shadow", + "value": "0.200000000000000011102230246251565404236316680908203125", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0.200000000000000011102230246251565404236316680908203125", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "hs_closed_amount": { + "versions": [ + { + "name": "hs_closed_amount", + "value": "0", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "hubspot_owner_id": { + "versions": [ + { + "name": "hubspot_owner_id", + "value": "823493108", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "823493108", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_created_by_user_id": { + "versions": [ + { + "name": "hs_created_by_user_id", + "value": "50549138", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50549138", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_createdate": { + "versions": [ + { + "name": "hs_createdate", + "value": "1680442574451", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680442574451", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_is_deal_split": { + "versions": [ + { + "name": "hs_is_deal_split", + "value": "false", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "false", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "hs_projected_amount": { + "versions": [ + { + "name": "hs_projected_amount", + "value": "10000000.000000000555111512312578270211815834045410156250000000", + "timestamp": 1680442574957, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "10000000.000000000555111512312578270211815834045410156250000000", + "timestamp": 1680442574957, + "persistenceTimestamp": 1680442574957, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": null + }, + "hs_num_target_accounts": { + "versions": [ + { + "name": "hs_num_target_accounts", + "value": "0", + "timestamp": 1680442574707, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574707, + "persistenceTimestamp": 1680442574707, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hs_all_owner_ids": { + "versions": [ + { + "name": "hs_all_owner_ids", + "value": "823493108", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "823493108", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_projected_amount_in_home_currency": { + "versions": [ + { + "name": "hs_projected_amount_in_home_currency", + "value": "10000000.000000000555111512312578270211815834045410156250000000", + "timestamp": 1680442574957, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "10000000.000000000555111512312578270211815834045410156250000000", + "timestamp": 1680442574957, + "persistenceTimestamp": 1680442574957, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": null + }, + "amount": { + "versions": [ + { + "name": "amount", + "value": "50000000", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50000000", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_is_closed_won": { + "versions": [ + { + "name": "hs_is_closed_won", + "value": "false", + "timestamp": 1680442574451, + "sourceId": "CalculatedPropertyComputer", + "source": "CALCULATED", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "false", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CALCULATED", + "sourceId": "CalculatedPropertyComputer", + "updatedByUserId": 50549138 + }, + "closedate": { + "versions": [ + { + "name": "closedate", + "value": "1682861749726", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1682861749726", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_num_associated_active_deal_registrations": { + "versions": [ + { + "name": "hs_num_associated_active_deal_registrations", + "value": "0", + "timestamp": 1680442574707, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574707, + "persistenceTimestamp": 1680442574707, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hs_user_ids_of_all_owners": { + "versions": [ + { + "name": "hs_user_ids_of_all_owners", + "value": "50549138", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50549138", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "pipeline": { + "versions": [ + { + "name": "pipeline", + "value": "default", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "default", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_lastmodifieddate": { + "versions": [ + { + "name": "hs_lastmodifieddate", + "value": "1680442574957", + "timestamp": 1680442574957, + "sourceId": "DealStageProbabilityHandler", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680442574957", + "timestamp": 1680442574957, + "persistenceTimestamp": 1680442574957, + "source": "CALCULATED", + "sourceId": "DealStageProbabilityHandler", + "updatedByUserId": null + }, + "hubspot_owner_assigneddate": { + "versions": [ + { + "name": "hubspot_owner_assigneddate", + "value": "1680442574451", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "1680442574451", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "dealstage": { + "versions": [ + { + "name": "dealstage", + "value": "appointmentscheduled", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "appointmentscheduled", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_num_associated_deal_splits": { + "versions": [ + { + "name": "hs_num_associated_deal_splits", + "value": "0", + "timestamp": 1680442574707, + "sourceId": "RollupProperties", + "source": "CALCULATED", + "sourceVid": [], + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "0", + "timestamp": 1680442574707, + "persistenceTimestamp": 1680442574707, + "source": "CALCULATED", + "sourceId": "RollupProperties", + "updatedByUserId": null + }, + "hs_object_id": { + "versions": [ + { + "name": "hs_object_id", + "value": "7036839888", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "7036839888", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + }, + "hs_updated_by_user_id": { + "versions": [ + { + "name": "hs_updated_by_user_id", + "value": "50549138", + "timestamp": 1680442574451, + "sourceId": "userId:50549138", + "source": "CRM_UI", + "sourceVid": [], + "requestId": "251ce58e-09b3-4b28-bbdd-6e8165d15a5d", + "updatedByUserId": 50549138, + "useTimestampAsPersistenceTimestamp": true + } + ], + "value": "50549138", + "timestamp": 1680442574451, + "persistenceTimestamp": 1680442574451, + "source": "CRM_UI", + "sourceId": "userId:50549138", + "updatedByUserId": 50549138 + } + }, + "version": 3, + "secondaryIdentifier": null, + "isDeleted": false + }, + "extraHeaders": { + "x-hubspot-client-ip": "172.21.156.49", + "x-hubspot-correlation-id": "0baa9845-a48c-415a-94a4-c33d32eb9347", + "x-hubspot-request-timestamp": "1680447533390", + "x-hubspot-requesting-chain-bin": "CkwKQEF1dG9tYXRpb25QbGF0Zm9ybUV4ZWN1dGlvbi1leGVjdXRpb25TbG93QWN0aW9uVHlwZUxhbmVXYWxXb3JrZXISCFBST0NGSUxFCjEKJUF1dG9tYXRpb25BY3Rpb25zRXhlY3V0aW9uU2VydmljZS13ZWISCFBST0NGSUxFEgIIAg==", + "x-hubspot-timeout-millis": "44610", + "x-trace": "2B98E110B8DA0D2F8CCB8D0A57BBE117FE0FEBCB20000000000000000000" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:hubspot/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "monday", + "providerLabel": "Monday", + "eventType": "create_pulse", + "name": "create_pulse", + "body": { + "event": { + "userId": 42973518, + "originalTriggerUuid": null, + "boardId": 4429449918, + "pulseId": 4429514497, + "pulseName": "New Item", + "groupId": "topics", + "groupName": "Group 1", + "groupColor": "#579bfc", + "isTopGroup": true, + "columnValues": {}, + "app": "monday", + "type": "create_pulse", + "triggerTime": "2023-05-07T06:16:58.471Z", + "subscriptionId": 227626570, + "triggerUuid": "1f418681359b1cc8f675efe2fd0ba062" + } + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:monday/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "monday", + "providerLabel": "Monday", + "eventType": "create_update", + "name": "create_update", + "body": { + "event": { + "userId": 42973518, + "originalTriggerUuid": null, + "boardId": 4429449918, + "pulseId": 4429462688, + "body": "

I'll check

", + "textBody": "I'll check", + "updateId": 2141354286, + "replyId": null, + "app": "monday", + "type": "create_update", + "triggerTime": "2023-05-07T06:18:47.778Z", + "subscriptionId": 227626549, + "triggerUuid": "ac4c0750983e5270bdfbb4cbcb65fd45" + } + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:monday/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "monday", + "providerLabel": "Monday", + "eventType": "move_pulse_into_group", + "name": "move_pulse_into_group", + "body": { + "event": { + "userId": 42973518, + "originalTriggerUuid": null, + "boardId": 4429449918, + "pulseId": 4429514497, + "sourceGroupId": "topics", + "destGroupId": "group_title", + "destGroup": { + "id": "group_title", + "title": "Group 2", + "color": "#a25ddc", + "is_top_group": false + }, + "app": "monday", + "type": "move_pulse_into_group", + "triggerTime": "2023-05-07T06:18:25.767Z", + "subscriptionId": 227626684, + "triggerUuid": "e412b412ac2d3cdd5684b4c1b57eb2f6" + } + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:monday/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "monday", + "providerLabel": "Monday", + "eventType": "update_column_value", + "name": "update_column_value", + "body": { + "event": { + "userId": 42973518, + "originalTriggerUuid": null, + "boardId": 4429449918, + "groupId": "topics", + "pulseId": 4429449927, + "pulseName": "Item 1", + "columnId": "status", + "columnType": "color", + "columnTitle": "Status", + "value": { + "label": { + "index": 1, + "text": "Done", + "style": { + "color": "#00c875", + "border": "#00B461", + "var_name": "green-shadow" + }, + "is_done": true + }, + "post_id": null + }, + "previousValue": { + "label": { + "index": 0, + "text": "Working on it", + "style": { + "color": "#fdab3d", + "border": "#E99729", + "var_name": "orange" + }, + "is_done": false + }, + "post_id": null + }, + "changedAt": 1683440337.5137901, + "isTopGroup": true, + "app": "monday", + "type": "update_column_value", + "triggerTime": "2023-05-07T06:18:57.914Z", + "subscriptionId": 227626788, + "triggerUuid": "998d11923305a73436d016ab2787df2e" + } + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:monday/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "campaign.approval.approved", + "name": "campaign.approval.approved", + "body": { + "type": "campaign.approval.approved", + "data": { + "approval": { + "id": "550e8400-e29b-41d4-a716-446655440710", + "isBlocking": true, + "message": "Legal sign-off needed for this campaign.", + "dueDate": "2025-03-05T17:00:00.000Z", + "createdAt": "2025-02-26T14:00:00.000Z", + "status": "Approved", + "approvedAt": "2025-02-26T16:15:00.000Z", + "approvedBy": { + "id": "550e8400-e29b-41d4-a716-446655440021", + "firstName": "Sam", + "lastName": "Chen", + "email": "sam@example.com" + }, + "requestedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + } + } + }, + "createdAt": "2025-02-26T16:15:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "campaign.approval.requested", + "name": "campaign.approval.requested", + "body": { + "type": "campaign.approval.requested", + "data": { + "approval": { + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "createdApprovals": [ + { + "id": "550e8400-e29b-41d4-a716-446655440710", + "isBlocking": true, + "message": "Legal sign-off needed for this campaign.", + "dueDate": "2025-03-05T17:00:00.000Z", + "createdAt": "2025-02-26T14:00:00.000Z", + "status": "Requested", + "user": { + "id": "550e8400-e29b-41d4-a716-446655440021", + "firstName": "Sam", + "lastName": "Chen", + "email": "sam@example.com" + }, + "requestedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + } + } + ], + "existingApprovals": [] + } + }, + "createdAt": "2025-02-26T14:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "invite.accepted", + "name": "invite.accepted", + "body": { + "type": "invite.accepted", + "data": { + "invite": { + "id": "550e8400-e29b-41d4-a716-446655440600", + "email": "newmember@example.com", + "createdAt": "2025-02-26T11:00:00.000Z", + "acceptedAt": "2025-02-26T14:30:00.000Z", + "invitedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "acceptedBy": { + "id": "550e8400-e29b-41d4-a716-446655440020", + "firstName": "John", + "lastName": "Smith", + "email": "newmember@example.com" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + } + } + }, + "createdAt": "2025-02-26T14:30:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "invite.created", + "name": "invite.created", + "body": { + "type": "invite.created", + "data": { + "invite": { + "id": "550e8400-e29b-41d4-a716-446655440600", + "email": "newmember@example.com", + "createdAt": "2025-02-26T11:00:00.000Z", + "invitedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "sentEmail": true + } + }, + "createdAt": "2025-02-26T11:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.approval.approved", + "name": "post.approval.approved", + "body": { + "type": "post.approval.approved", + "data": { + "approval": { + "id": "550e8400-e29b-41d4-a716-446655440700", + "isBlocking": true, + "message": "Please review before we publish.", + "dueDate": "2025-02-28T17:00:00.000Z", + "createdAt": "2025-02-26T14:00:00.000Z", + "status": "Approved", + "approvedAt": "2025-02-26T15:30:00.000Z", + "approvedBy": { + "id": "550e8400-e29b-41d4-a716-446655440020", + "firstName": "Alex", + "lastName": "Rivera", + "email": "alex@example.com" + }, + "requestedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001" + }, + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + } + } + }, + "createdAt": "2025-02-26T15:30:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.approval.requested", + "name": "post.approval.requested", + "body": { + "type": "post.approval.requested", + "data": { + "approval": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001" + }, + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "createdApprovals": [ + { + "id": "550e8400-e29b-41d4-a716-446655440700", + "isBlocking": true, + "message": "Please review before we publish.", + "dueDate": "2025-02-28T17:00:00.000Z", + "createdAt": "2025-02-26T14:00:00.000Z", + "status": "Requested", + "user": { + "id": "550e8400-e29b-41d4-a716-446655440020", + "firstName": "Alex", + "lastName": "Rivera", + "email": "alex@example.com" + }, + "requestedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + } + } + ], + "existingApprovals": [] + } + }, + "createdAt": "2025-02-26T14:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.archived", + "name": "post.archived", + "body": { + "type": "post.archived", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Draft Post", + "status": "archived", + "channels": [ + "LinkedIn", + "Twitter" + ], + "campaign": null, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "archivedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "archivedAt": "2025-02-26T15:00:00.000Z", + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Draft post content for LinkedIn", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440004", + "name": "Acme Inc", + "detail": "@acme_inc" + }, + "tweets": [ + { + "copy": "Draft post content for Twitter", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440011", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + } + ] + } + } + }, + "createdAt": "2025-02-26T15:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.comment.created", + "name": "post.comment.created", + "body": { + "type": "post.comment.created", + "data": { + "comment": { + "id": "550e8400-e29b-41d4-a716-446655440050", + "message": "Looks good! Let's add a CTA at the end.", + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "createdBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "createdAt": "2025-02-26T16:00:00.000Z" + } + }, + "createdAt": "2025-02-26T16:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.content.edited", + "name": "post.content.edited", + "body": { + "type": "post.content.edited", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "channels": [ + "LinkedIn", + "Twitter" + ], + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "editedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "editedAt": "2025-02-26T15:30:00.000Z", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Updated LinkedIn content for the launch!", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440004", + "name": "Acme Inc", + "detail": "@acme_inc" + }, + "tweets": [ + { + "copy": "Updated Twitter content for the launch!", + "assets": [] + } + ] + } + } + }, + "createdAt": "2025-02-26T15:30:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.created", + "name": "post.created", + "body": { + "type": "post.created", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "New Draft", + "status": "draft", + "channels": [ + "LinkedIn" + ], + "campaign": null, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "createdBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "createdAt": "2025-02-26T12:00:00.000Z", + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Excited to share our latest product update!", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": null + } + }, + "createdAt": "2025-02-26T12:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.inline_comment.created", + "name": "post.inline_comment.created", + "body": { + "type": "post.inline_comment.created", + "data": { + "comment": { + "thread": { + "id": "550e8400-e29b-41d4-a716-446655440060", + "channel": "LinkedIn", + "highlightedText": "coming next week", + "resolved": false, + "createdAt": "2025-02-26T16:05:00.000Z", + "updatedAt": null + }, + "id": "550e8400-e29b-41d4-a716-446655440051", + "message": "Can we change this to 'launching soon'?", + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "createdBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "createdAt": "2025-02-26T16:05:00.000Z" + } + }, + "createdAt": "2025-02-26T16:05:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.permanently_deleted", + "name": "post.permanently_deleted", + "body": { + "type": "post.permanently_deleted", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Old Draft", + "status": "archived", + "channels": [ + "LinkedIn" + ], + "campaign": null, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "deletedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "deletedAt": "2025-03-15T10:00:00.000Z", + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Old draft content", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": null + } + }, + "createdAt": "2025-03-15T10:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.publish_failed", + "name": "post.publish_failed", + "body": { + "type": "post.publish_failed", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "channel": "LinkedIn", + "error": "Token expired", + "campaign": null, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "scheduledPublishAt": "2025-03-01T14:00:00.000Z", + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "createdBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "failedAt": "2025-03-01T14:00:05.000Z" + } + }, + "createdAt": "2025-03-01T14:00:05.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.published", + "name": "post.published", + "body": { + "type": "post.published", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "channel": "LinkedIn", + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "postUrl": "https://www.linkedin.com/feed/update/urn:li:share:7123456789012345678", + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "publishedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "publishedAt": "2025-02-26T14:30:00.000Z" + } + }, + "createdAt": "2025-02-26T14:30:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.rescheduled", + "name": "post.rescheduled", + "body": { + "type": "post.rescheduled", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "channels": [ + "LinkedIn" + ], + "campaign": null, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "previousPublishAt": "2025-03-01T14:00:00.000Z", + "newPublishAt": "2025-03-01T16:00:00.000Z", + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "rescheduledBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "rescheduledAt": "2025-02-26T13:00:00.000Z", + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Excited to announce our new product launch!", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": null + } + }, + "createdAt": "2025-02-26T13:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.scheduled", + "name": "post.scheduled", + "body": { + "type": "post.scheduled", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "channels": [ + "LinkedIn", + "Twitter" + ], + "campaign": { + "id": "550e8400-e29b-41d4-a716-446655440003", + "name": "Launch 2025", + "startDate": "2025-01-01", + "endDate": "2025-12-31" + }, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "publishAt": "2025-03-01T14:00:00.000Z", + "postedImmediately": false, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "scheduledBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "scheduledAt": "2025-02-26T12:00:00.000Z", + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Excited to announce our new product launch!", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440004", + "name": "Acme Inc", + "detail": "@acme_inc" + }, + "tweets": [ + { + "copy": "Excited to announce our new product launch!", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440011", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + } + ] + } + } + }, + "createdAt": "2025-02-26T12:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "post.unscheduled", + "name": "post.unscheduled", + "body": { + "type": "post.unscheduled", + "data": { + "post": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "channels": [ + "LinkedIn" + ], + "campaign": null, + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", + "labels": [ + { + "id": "550e8400-e29b-41d4-a716-446655440400", + "name": "Marketing", + "color": "#9333ea", + "backgroundColor": "#f3e8ff" + } + ], + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "unscheduledBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "unscheduledAt": "2025-02-26T14:00:00.000Z", + "linkedIn": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc" + }, + "copy": "Excited to announce our new product launch!", + "assets": [ + { + "id": "550e8400-e29b-41d4-a716-446655440010", + "name": "product-photo.jpg", + "url": "https://cdn.tryordinal.com/assets/product-photo.jpg", + "mimetype": "image/jpeg", + "size": 245678, + "width": 1920, + "height": 1080, + "duration": null + } + ] + }, + "x": null + } + }, + "createdAt": "2025-02-26T14:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "social_profile.connected", + "name": "social_profile.connected", + "body": { + "type": "social_profile.connected", + "data": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc", + "channel": "LinkedIn", + "profileImageUrl": "https://cdn.tryordinal.com/profiles/acme.png", + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "connectedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "connectedAt": "2025-02-26T10:00:00.000Z" + } + }, + "createdAt": "2025-02-26T10:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "social_profile.disconnected", + "name": "social_profile.disconnected", + "body": { + "type": "social_profile.disconnected", + "data": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc", + "channel": "LinkedIn", + "profileImageUrl": null, + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "disconnectedBy": { + "id": "550e8400-e29b-41d4-a716-446655440010", + "firstName": "Jane", + "lastName": "Doe", + "email": "jane@example.com" + }, + "disconnectedAt": "2025-02-26T11:00:00.000Z", + "unscheduledPosts": [ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "title": "Q4 Product Launch Announcement", + "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001" + } + ] + } + }, + "createdAt": "2025-02-26T11:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "ordinal", + "providerLabel": "Ordinal", + "eventType": "social_profile.reconnect_needed", + "name": "social_profile.reconnect_needed", + "body": { + "type": "social_profile.reconnect_needed", + "data": { + "profile": { + "id": "550e8400-e29b-41d4-a716-446655440002", + "name": "Acme Inc", + "detail": "acme-inc", + "channel": "LinkedIn", + "profileImageUrl": "https://cdn.tryordinal.com/profiles/acme.png", + "workspace": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "slug": "acme", + "name": "Acme Inc" + }, + "detectedAt": "2025-02-26T09:00:00.000Z" + } + }, + "createdAt": "2025-02-26T09:00:00.000Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:ordinal/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "address.created", + "name": "address.created", + "body": { + "data": { + "id": "add_01j7br89t3wgn5v5w49hm1mepv", + "city": null, + "region": null, + "status": "active", + "created_at": "2024-09-09T16:00:47.939Z", + "first_line": null, + "updated_at": "2024-09-09T16:00:47.939Z", + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "description": null, + "import_meta": null, + "postal_code": "10021", + "second_line": null, + "country_code": "US" + }, + "event_id": "evt_01j7br8a9wah2vavmp3pe9k6r1", + "event_type": "address.created", + "occurred_at": "2024-09-09T16:00:48.444751Z", + "notification_id": "ntf_01j7br8ap7fjjm2424q81qt5zc" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "adjustment.created", + "name": "adjustment.created", + "body": { + "data": { + "id": "adj_01j7brvp4836axtqpwvpgdssh7", + "items": [ + { + "id": "adjitm_01j7brvp4836axtqpwvqa6z30p", + "type": "full", + "amount": "10887", + "totals": { + "tax": "887", + "total": "10887", + "subtotal": "10000" + }, + "item_id": "txnitm_01j7br8aez018mgmrj3bpmxmh7", + "proration": null + } + ], + "action": "refund", + "reason": "error", + "status": "pending_approval", + "totals": { + "fee": "553", + "tax": "887", + "total": "10887", + "earnings": "9447", + "subtotal": "10000", + "currency_code": "USD" + }, + "created_at": "2024-09-09T16:11:23.156526Z", + "updated_at": "2024-09-09T16:11:23Z", + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "currency_code": "USD", + "payout_totals": { + "fee": "553", + "tax": "887", + "total": "10887", + "earnings": "9447", + "subtotal": "10000", + "currency_code": "USD" + }, + "transaction_id": "txn_01j7br78cgr083zqw1jhqym40s", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq", + "credit_applied_to_balance": null + }, + "event_id": "evt_01j7brvpf76ndzrenetzbk8m0x", + "event_type": "adjustment.created", + "occurred_at": "2024-09-09T16:11:23.495326Z", + "notification_id": "ntf_01j7brvpnh3dbqmpnz2skccsq0" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "adjustment.updated", + "name": "adjustment.updated", + "body": { + "data": { + "id": "adj_01j7brvp4836axtqpwvpgdssh7", + "items": [ + { + "id": "adjitm_01j7brvp4836axtqpwvqa6z30p", + "type": "full", + "amount": "10887", + "totals": { + "tax": "887", + "total": "10887", + "subtotal": "10000" + }, + "item_id": "txnitm_01j7br8aez018mgmrj3bpmxmh7", + "proration": null + } + ], + "action": "refund", + "reason": "error", + "status": "approved", + "totals": { + "fee": "553", + "tax": "887", + "total": "10887", + "earnings": "9447", + "subtotal": "10000", + "currency_code": "USD" + }, + "created_at": "2024-09-09T16:11:23.156526Z", + "updated_at": "2024-09-09T16:20:01.336183Z", + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "currency_code": "USD", + "payout_totals": { + "fee": "553", + "tax": "887", + "total": "10887", + "earnings": "9447", + "subtotal": "10000", + "currency_code": "USD" + }, + "transaction_id": "txn_01j7br78cgr083zqw1jhqym40s", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq", + "credit_applied_to_balance": null + }, + "event_id": "evt_01j7bsbgpvc60cc727n9k76xcd", + "event_type": "adjustment.updated", + "occurred_at": "2024-09-09T16:20:01.883390Z", + "notification_id": "ntf_01j7bsbh2d217449az4tf0k7m6" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "customer.created", + "name": "customer.created", + "body": { + "data": { + "id": "ctm_01j7br89shp7xpaenx22h8qetw", + "name": null, + "email": "sam@example.com", + "locale": "en", + "status": "active", + "created_at": "2024-09-09T16:00:47.921Z", + "updated_at": "2024-09-09T16:00:47.921Z", + "custom_data": null, + "import_meta": null, + "marketing_consent": false + }, + "event_id": "evt_01j7br8aax25dzcc8xe1xq32d0", + "event_type": "customer.created", + "occurred_at": "2024-09-09T16:00:48.477852Z", + "notification_id": "ntf_01j7br8ancx88m6hp38d3ecvab" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "subscription.activated", + "name": "subscription.activated", + "body": { + "data": { + "id": "sub_01j7br9jpk4rmmcd5crn2th1bq", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 10, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-09-09T16:01:29.811Z", + "trial_dates": null, + "next_billed_at": "2024-10-09T16:01:29.378686Z", + "previously_billed_at": "2024-09-09T16:01:29.378686Z" + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 1, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-09-09T16:01:29.811Z", + "trial_dates": null, + "next_billed_at": "2024-10-09T16:01:29.378686Z", + "previously_billed_at": "2024-09-09T16:01:29.378686Z" + } + ], + "status": "active", + "discount": null, + "paused_at": null, + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:01:29.811Z", + "started_at": "2024-09-09T16:01:29.378686Z", + "updated_at": "2024-09-09T16:01:29.811Z", + "business_id": null, + "canceled_at": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "import_meta": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "currency_code": "USD", + "next_billed_at": "2024-10-09T16:01:29.378686Z", + "billing_details": null, + "collection_mode": "automatic", + "first_billed_at": "2024-09-09T16:01:29.378686Z", + "scheduled_change": null, + "current_billing_period": { + "ends_at": "2024-10-09T16:01:29.378686Z", + "starts_at": "2024-09-09T16:01:29.378686Z" + } + }, + "event_id": "evt_01j7br9m1s4bh0db0sb8n9wfdp", + "event_type": "subscription.activated", + "occurred_at": "2024-09-09T16:01:31.193492Z", + "notification_id": "ntf_01j7br9m6xfnajmm0mbr0zm0tg" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "subscription.created", + "name": "subscription.created", + "body": { + "data": { + "id": "sub_01j7br9jpk4rmmcd5crn2th1bq", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 10, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-09-09T16:01:29.811Z", + "trial_dates": null, + "next_billed_at": "2024-10-09T16:01:29.378686Z", + "previously_billed_at": "2024-09-09T16:01:29.378686Z" + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 1, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-09-09T16:01:29.811Z", + "trial_dates": null, + "next_billed_at": "2024-10-09T16:01:29.378686Z", + "previously_billed_at": "2024-09-09T16:01:29.378686Z" + } + ], + "status": "active", + "discount": null, + "paused_at": null, + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:01:29.811Z", + "started_at": "2024-09-09T16:01:29.378686Z", + "updated_at": "2024-09-09T16:01:29.811Z", + "business_id": null, + "canceled_at": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "import_meta": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "currency_code": "USD", + "next_billed_at": "2024-10-09T16:01:29.378686Z", + "transaction_id": "txn_01j7br78cgr083zqw1jhqym40s", + "billing_details": null, + "collection_mode": "automatic", + "first_billed_at": "2024-09-09T16:01:29.378686Z", + "scheduled_change": null, + "current_billing_period": { + "ends_at": "2024-10-09T16:01:29.378686Z", + "starts_at": "2024-09-09T16:01:29.378686Z" + } + }, + "event_id": "evt_01j7br9m2aesan7r77ckkast3s", + "event_type": "subscription.created", + "occurred_at": "2024-09-09T16:01:31.210540Z", + "notification_id": "ntf_01j7br9m7y1vm3ms0jxd7jf8v9" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "subscription.past_due", + "name": "subscription.past_due", + "body": { + "data": { + "id": "sub_01j7br9jpk4rmmcd5crn2th1bq", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 10, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-10-09T16:01:39.146686Z", + "trial_dates": null, + "next_billed_at": "2024-11-09T16:01:29.378686Z", + "previously_billed_at": "2024-10-09T16:01:29.378686Z" + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 1, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-10-09T16:01:39.147686Z", + "trial_dates": null, + "next_billed_at": "2024-11-09T16:01:29.378686Z", + "previously_billed_at": "2024-10-09T16:01:29.378686Z" + } + ], + "status": "past_due", + "discount": null, + "paused_at": null, + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:01:29.811Z", + "started_at": "2024-09-09T16:01:29.378686Z", + "updated_at": "2024-10-09T16:01:39.148686Z", + "business_id": null, + "canceled_at": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "import_meta": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "currency_code": "USD", + "next_billed_at": "2024-11-09T16:01:29.378686Z", + "billing_details": null, + "collection_mode": "automatic", + "first_billed_at": "2024-09-09T16:01:29.378686Z", + "scheduled_change": null, + "current_billing_period": { + "ends_at": "2024-11-09T16:01:29.378686Z", + "starts_at": "2024-10-09T16:01:29.378686Z" + } + }, + "event_id": "evt_01j7breb96dre2qv2bf7eq07d3", + "event_type": "subscription.past_due", + "occurred_at": "2024-09-09T16:04:06.054130Z", + "notification_id": "ntf_01j7brebh5yztyk1f98vyj25xt" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "subscription.updated", + "name": "subscription.updated", + "body": { + "data": { + "id": "sub_01j7br9jpk4rmmcd5crn2th1bq", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 10, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-10-09T16:01:39.146686Z", + "trial_dates": null, + "next_billed_at": "2024-11-09T16:01:29.378686Z", + "previously_billed_at": "2024-10-09T16:01:29.378686Z" + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "import_meta": null, + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "status": "active", + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "import_meta": null, + "tax_category": "standard" + }, + "quantity": 1, + "recurring": true, + "created_at": "2024-09-09T16:01:29.811Z", + "updated_at": "2024-10-09T16:01:39.147686Z", + "trial_dates": null, + "next_billed_at": "2024-11-09T16:01:29.378686Z", + "previously_billed_at": "2024-10-09T16:01:29.378686Z" + } + ], + "status": "past_due", + "discount": null, + "paused_at": null, + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:01:29.811Z", + "started_at": "2024-09-09T16:01:29.378686Z", + "updated_at": "2024-10-09T16:01:39.148686Z", + "business_id": null, + "canceled_at": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "import_meta": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "currency_code": "USD", + "next_billed_at": "2024-11-09T16:01:29.378686Z", + "billing_details": null, + "collection_mode": "automatic", + "first_billed_at": "2024-09-09T16:01:29.378686Z", + "scheduled_change": null, + "current_billing_period": { + "ends_at": "2024-11-09T16:01:29.378686Z", + "starts_at": "2024-10-09T16:01:29.378686Z" + } + }, + "event_id": "evt_01j7breb93syekfvzq86r4940f", + "event_type": "subscription.updated", + "occurred_at": "2024-09-09T16:04:06.051917Z", + "notification_id": "ntf_01j7brebggpc2gcq38bzpphzmd" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.billed", + "name": "transaction.billed", + "body": { + "data": { + "id": "txn_01j7bre7fka75pa1k8arkf0by3", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + } + ], + "origin": "subscription_recurring", + "status": "billed", + "details": { + "totals": { + "fee": null, + "tax": "3549", + "total": "43549", + "credit": "0", + "balance": "43549", + "discount": "0", + "earnings": null, + "subtotal": "40000", + "grand_total": "43549", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7bre7vtfvxd0mvhr7s4x55h", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7bre7vvfcpg0hjkgypbp002", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "3549", + "total": "43549", + "discount": "0", + "subtotal": "40000" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "3549", + "total": "43549", + "earnings": "0", + "subtotal": "40000", + "grand_total": "43549", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7bre7fka75pa1k8arkf0by3" + }, + "payments": [], + "billed_at": "2024-09-09T16:04:02.163775934Z", + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:04:02.613735612Z", + "invoice_id": null, + "updated_at": "2024-09-09T16:04:02.613735612Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-11-09T16:01:29.378686Z", + "starts_at": "2024-10-09T16:01:29.378686Z" + }, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq" + }, + "event_id": "evt_01j7bre89dmgqdecwpznemhjxz", + "event_type": "transaction.billed", + "occurred_at": "2024-09-09T16:04:02.989795Z", + "notification_id": "ntf_01j7bre8fzam079nxvx1808trs" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.completed", + "name": "transaction.completed", + "body": { + "data": { + "id": "txn_01j7br78cgr083zqw1jhqym40s", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "completed", + "details": { + "totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7br8aeyz8r6wm2zywjw97d4", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3bpmxmh7", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3e9nbbr7", + "totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + }, + "item_id": null, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "fee_rate": "0.05", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "exchange_rate": "1", + "credit_to_balance": "0" + }, + "tax_rates_used": [ + { + "totals": { + "tax": "5315", + "total": "65215", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7br78cgr083zqw1jhqym40s" + }, + "payments": [ + { + "amount": "65215", + "status": "captured", + "created_at": "2024-09-09T16:01:17.683756Z", + "error_code": null, + "captured_at": "2024-09-09T16:01:29.378686Z", + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2029, + "expiry_month": 10, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01j7br96tsmrhw9zef1dc0rmef", + "payment_attempt_id": "01e1c65b-3eca-4fca-b160-f162d641852e", + "stored_payment_method_id": "9708381b-bd52-4c65-abc0-705e636c5c47" + } + ], + "billed_at": "2024-09-09T16:01:29.536914Z", + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:00:13.888989Z", + "invoice_id": "inv_01j7br9jq8k57phrbkyehe9kwj", + "updated_at": "2024-09-09T16:01:31.565947359Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-10-09T16:01:29.378686Z", + "starts_at": "2024-09-09T16:01:29.378686Z" + }, + "invoice_number": "325-11889", + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq" + }, + "event_id": "evt_01j7br9myw2s8wzb7t8x73f7sn", + "event_type": "transaction.completed", + "occurred_at": "2024-09-09T16:01:32.124939Z", + "notification_id": "ntf_01j7br9n4yhaq2r7hzndzaa33d" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.created", + "name": "transaction.created", + "body": { + "data": { + "id": "txn_01j7br78cgr083zqw1jhqym40s", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "draft", + "details": { + "totals": { + "fee": null, + "tax": "9157", + "total": "54940", + "credit": "0", + "balance": "54940", + "discount": "0", + "earnings": null, + "subtotal": "45783", + "grand_total": "54940", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7br78hpq4j57aq69gp6cvp8", + "totals": { + "tax": "4586", + "total": "27516", + "discount": "0", + "subtotal": "22930" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "unit_totals": { + "tax": "459", + "total": "2752", + "discount": "0", + "subtotal": "2293" + } + }, + { + "id": "txnitm_01j7br78hpq4j57aq69p5eh0ra", + "totals": { + "tax": "1529", + "total": "9172", + "discount": "0", + "subtotal": "7643" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.2", + "unit_totals": { + "tax": "1529", + "total": "9172", + "discount": "0", + "subtotal": "7643" + } + }, + { + "id": "txnitm_01j7br78hpq4j57aq69sc1dxje", + "totals": { + "tax": "3042", + "total": "18252", + "discount": "0", + "subtotal": "15210" + }, + "item_id": null, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.2", + "unit_totals": { + "tax": "3042", + "total": "18252", + "discount": "0", + "subtotal": "15210" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "9157", + "total": "54940", + "discount": "0", + "subtotal": "45783" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "9157", + "total": "54940", + "earnings": "0", + "subtotal": "45783", + "grand_total": "54940", + "currency_code": "GBP" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7br78cgr083zqw1jhqym40s" + }, + "payments": [], + "billed_at": null, + "address_id": null, + "created_at": "2024-09-09T16:00:13.888989481Z", + "invoice_id": null, + "updated_at": "2024-09-09T16:00:13.888989481Z", + "business_id": null, + "custom_data": null, + "customer_id": null, + "discount_id": null, + "receipt_data": null, + "currency_code": "GBP", + "billing_period": null, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": null + }, + "event_id": "evt_01j7br78zywewybejv0njrzhan", + "event_type": "transaction.created", + "occurred_at": "2024-09-09T16:00:14.334401Z", + "notification_id": "ntf_01j7br797ss25hp5rnshksjq6y" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.paid", + "name": "transaction.paid", + "body": { + "data": { + "id": "txn_01j7br78cgr083zqw1jhqym40s", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "paid", + "details": { + "totals": { + "fee": null, + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": null, + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7br8aeyz8r6wm2zywjw97d4", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3bpmxmh7", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3e9nbbr7", + "totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + }, + "item_id": null, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "5315", + "total": "65215", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "5315", + "total": "65215", + "earnings": "0", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7br78cgr083zqw1jhqym40s" + }, + "payments": [ + { + "amount": "65215", + "status": "captured", + "created_at": "2024-09-09T16:01:17.683756Z", + "error_code": null, + "captured_at": "2024-09-09T16:01:29.378686Z", + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2029, + "expiry_month": 10, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01j7br96tsmrhw9zef1dc0rmef", + "payment_attempt_id": "01e1c65b-3eca-4fca-b160-f162d641852e", + "stored_payment_method_id": "9708381b-bd52-4c65-abc0-705e636c5c47" + } + ], + "billed_at": "2024-09-09T16:01:29.53691403Z", + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:00:13.888989Z", + "invoice_id": null, + "updated_at": "2024-09-09T16:01:29.536915778Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": null, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": null + }, + "event_id": "evt_01j7br9jtryxbtd89yzj3pkyec", + "event_type": "transaction.paid", + "occurred_at": "2024-09-09T16:01:29.944764Z", + "notification_id": "ntf_01j7br9k4jznazhz99sb26z34t" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.past_due", + "name": "transaction.past_due", + "body": { + "data": { + "id": "txn_01j7bre7fka75pa1k8arkf0by3", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + } + ], + "origin": "subscription_recurring", + "status": "past_due", + "details": { + "totals": { + "fee": null, + "tax": "3549", + "total": "43549", + "credit": "0", + "balance": "43549", + "discount": "0", + "earnings": null, + "subtotal": "40000", + "grand_total": "43549", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7bre7vtfvxd0mvhr7s4x55h", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7bre7vvfcpg0hjkgypbp002", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "3549", + "total": "43549", + "discount": "0", + "subtotal": "40000" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "3549", + "total": "43549", + "earnings": "0", + "subtotal": "40000", + "grand_total": "43549", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7bre7fka75pa1k8arkf0by3" + }, + "payments": [ + { + "amount": "43549", + "status": "error", + "created_at": "2024-09-09T16:04:02.697051Z", + "error_code": "authentication_failed", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2029, + "expiry_month": 10, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01j7br96tsmrhw9zef1dc0rmef", + "payment_attempt_id": "58c9c9a3-593c-428b-a2d4-cfc1603d4c36", + "stored_payment_method_id": "9708381b-bd52-4c65-abc0-705e636c5c47" + } + ], + "billed_at": "2024-09-09T16:04:02.163775Z", + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:04:02.613735Z", + "invoice_id": null, + "updated_at": "2024-09-09T16:04:04.278528938Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-11-09T16:01:29.378686Z", + "starts_at": "2024-10-09T16:01:29.378686Z" + }, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq" + }, + "event_id": "evt_01j7brea32v1fg5hzt0fdyc7x1", + "event_type": "transaction.past_due", + "occurred_at": "2024-09-09T16:04:04.834513Z", + "notification_id": "ntf_01j7breaj7w2vemrk7f92vmgbj" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.payment_failed", + "name": "transaction.payment_failed", + "body": { + "data": { + "id": "txn_01j7bre7fka75pa1k8arkf0by3", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + } + ], + "origin": "subscription_recurring", + "status": "billed", + "details": { + "totals": { + "fee": null, + "tax": "3549", + "total": "43549", + "credit": "0", + "balance": "43549", + "discount": "0", + "earnings": null, + "subtotal": "40000", + "grand_total": "43549", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7bre7vtfvxd0mvhr7s4x55h", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7bre7vvfcpg0hjkgypbp002", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "3549", + "total": "43549", + "discount": "0", + "subtotal": "40000" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "3549", + "total": "43549", + "earnings": "0", + "subtotal": "40000", + "grand_total": "43549", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7bre7fka75pa1k8arkf0by3" + }, + "payments": [ + { + "amount": "43549", + "status": "error", + "created_at": "2024-09-09T16:04:02.697051Z", + "error_code": "authentication_failed", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2029, + "expiry_month": 10, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01j7br96tsmrhw9zef1dc0rmef", + "payment_attempt_id": "58c9c9a3-593c-428b-a2d4-cfc1603d4c36", + "stored_payment_method_id": "9708381b-bd52-4c65-abc0-705e636c5c47" + } + ], + "billed_at": "2024-09-09T16:04:02.163775Z", + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:04:02.613735Z", + "invoice_id": null, + "updated_at": "2024-09-09T16:04:02.613735Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-11-09T16:01:29.378686Z", + "starts_at": "2024-10-09T16:01:29.378686Z" + }, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq" + }, + "event_id": "evt_01j7brea33qcp49ab8c1n6q034", + "event_type": "transaction.payment_failed", + "occurred_at": "2024-09-09T16:04:04.835508Z", + "notification_id": "ntf_01j7breaj8kq311b1514ytt040" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.ready", + "name": "transaction.ready", + "body": { + "data": { + "id": "txn_01j7br78cgr083zqw1jhqym40s", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "ready", + "details": { + "totals": { + "fee": null, + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "65215", + "discount": "0", + "earnings": null, + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7br8aeyz8r6wm2zywjw97d4", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3bpmxmh7", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3e9nbbr7", + "totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + }, + "item_id": null, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "5315", + "total": "65215", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "5315", + "total": "65215", + "earnings": "0", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7br78cgr083zqw1jhqym40s" + }, + "payments": [], + "billed_at": null, + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:00:13.888989Z", + "invoice_id": null, + "updated_at": "2024-09-09T16:00:48.609971244Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": null, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": null + }, + "event_id": "evt_01j7br8avv2rjxwfgkqt26232e", + "event_type": "transaction.ready", + "occurred_at": "2024-09-09T16:00:49.019800Z", + "notification_id": "ntf_01j7br8b12tc3egc4ej67je35y" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddlebilling", + "providerLabel": "Paddle Billing", + "eventType": "transaction.updated", + "name": "transaction.updated", + "body": { + "data": { + "id": "txn_01j7br78cgr083zqw1jhqym40s", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "name": "Monthly (per seat)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T13:55:22.538367Z", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "updated_at": "2024-04-11T13:54:52.254748Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "name": "Monthly (recurring addon)", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-06-01T13:31:12.625056Z", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "updated_at": "2024-04-12T10:42:45.476453Z", + "custom_data": null, + "description": "Monthly", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "name": "One-time addon", + "type": "standard", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "created_at": "2023-02-23T14:01:28.391712Z", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "updated_at": "2024-04-09T07:23:10.921392Z", + "custom_data": null, + "description": "One-time addon", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "completed", + "details": { + "totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01j7br8aeyz8r6wm2zywjw97d4", + "totals": { + "tax": "2662", + "total": "32662", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "AeroEdit Pro", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/bT1XUOJAQhOUxGs83cbk_pro.png", + "created_at": "2023-02-23T12:43:46.605Z", + "updated_at": "2024-04-05T15:53:44.687Z", + "custom_data": { + "features": { + "sso": false, + "route_planning": true, + "payment_by_invoice": false, + "aircraft_performance": true, + "compliance_monitoring": true, + "flight_log_management": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of aircraft performance, advanced route planning, and compliance monitoring." + }, + "description": "Designed for professional pilots, including all features plus in Basic plus compliance monitoring, route optimization, and third-party integrations.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "266", + "total": "3266", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3bpmxmh7", + "totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Analytics addon", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/97dRpA6SXzcE6ekK9CAr_analytics.png", + "created_at": "2023-06-01T13:30:50.302Z", + "updated_at": "2024-04-05T15:47:17.163Z", + "custom_data": null, + "description": "Unlock advanced insights into your flight data with enhanced analytics and reporting features. Includes customizable reporting templates and trend analysis across flights.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "887", + "total": "10887", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01j7br8aez018mgmrj3e9nbbr7", + "totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + }, + "item_id": null, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "type": "standard", + "status": "active", + "image_url": "https://paddle.s3.amazonaws.com/user/165798/XIG7UXoJQHmlIAiKcnkA_custom-domains.png", + "created_at": "2023-02-23T14:01:02.441Z", + "updated_at": "2024-04-05T15:43:28.971Z", + "custom_data": null, + "description": "Make AeroEdit truly your own with custom domains. Custom domains reinforce your brand identity and make it easy for your team to access your account.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.08875", + "unit_totals": { + "tax": "1766", + "total": "21666", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "56589", + "fee_rate": "0.05", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD", + "exchange_rate": "1", + "credit_to_balance": "0" + }, + "tax_rates_used": [ + { + "totals": { + "tax": "5315", + "total": "65215", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.08875" + } + ], + "adjusted_totals": { + "fee": "3311", + "tax": "5315", + "total": "65215", + "earnings": "56589", + "subtotal": "59900", + "grand_total": "65215", + "currency_code": "USD" + } + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01j7br78cgr083zqw1jhqym40s" + }, + "payments": [ + { + "amount": "65215", + "status": "captured", + "created_at": "2024-09-09T16:01:17.683756Z", + "error_code": null, + "captured_at": "2024-09-09T16:01:29.378686Z", + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2029, + "expiry_month": 10, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_method_id": "paymtd_01j7br96tsmrhw9zef1dc0rmef", + "payment_attempt_id": "01e1c65b-3eca-4fca-b160-f162d641852e", + "stored_payment_method_id": "9708381b-bd52-4c65-abc0-705e636c5c47" + } + ], + "billed_at": "2024-09-09T16:01:29.536914Z", + "address_id": "add_01j7br89t3wgn5v5w49hm1mepv", + "created_at": "2024-09-09T16:00:13.888989Z", + "invoice_id": "inv_01j7br9jq8k57phrbkyehe9kwj", + "updated_at": "2024-09-09T16:01:31.565947359Z", + "business_id": null, + "custom_data": null, + "customer_id": "ctm_01j7br89shp7xpaenx22h8qetw", + "discount_id": null, + "receipt_data": null, + "currency_code": "USD", + "billing_period": { + "ends_at": "2024-10-09T16:01:29.378686Z", + "starts_at": "2024-09-09T16:01:29.378686Z" + }, + "invoice_number": "325-11889", + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": "sub_01j7br9jpk4rmmcd5crn2th1bq" + }, + "event_id": "evt_01j7br9myygc09mszbeh4d808h", + "event_type": "transaction.updated", + "occurred_at": "2024-09-09T16:01:32.126219Z", + "notification_id": "ntf_01j7br9n5mk8wgrdcfj6y03xey" + }, + "extraHeaders": { + "paddle-version": "1" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddlebilling/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "payment_dispute_closed", + "name": "payment_dispute_closed", + "body": { + "alert_id": "1467192647", + "alert_name": "payment_dispute_closed", + "amount": "689.54", + "balance_amount": "89.39", + "balance_currency": "EUR", + "balance_fee": "362.12", + "checkout_id": "9-f7f8b847ac4908e-89357ebf6e", + "currency": "EUR", + "custom_data": "custom_data", + "email": "qmohr@example.net", + "event_time": "2023-03-08 09:33:44", + "fee_usd": "0.51", + "marketing_consent": "", + "order_id": "9", + "passthrough": "Example String", + "status": "closed", + "p_signature": "G6+OBImxbqBSJm0FJX2fpJYNxsm/gzS9qcdtYmy560f5a2GEzS600AI0l2/hIel8i3AaqytYUMxxr7XfI/ixY/sxQ/lMd7LTqnEfMIhQ1aJ+iEjbtKdceEEXiCcVwSZt3ACKv8Ce+CZDHmVy/ebdEFCwwalucqZH6mMyMqXRBzLs/5K1c6JIvIz65vktfJWJuuNvu55Bu0oydHLAV8tJYgDQ08hjW19ZuaLpi05zz+VFYGh2txz6t/lnhInQKg9rv+0gvIDTFi9LJn79dqx6O2MjSiBhmSLEadk3Z9+9PT/6AUGJEiCZmZ54bimddDRmXMQLcNFRb5StVyIGgYlIXHtJQPg3OzHFxfCMiuSim29Rxb7ILJlBWar655Ctcjw3aSK97W/lB/60uZADslRduxMANDtOw+KQrvWLcLTL7ymTx1WN5c43U6sjglWVU3lpVMlV/1p/xJAFTmoF/9NvXSZsI4Zmxq5QKF8FfbpLMud3veMLzwUeiiVmlMPLbKZDq2smu1gEKwQ+jPrFytb+ssa6T+NXcwsWDSyLhZ/eFZ+bYpei9AO2mwHYHB5LEiVodP+U7VIz/Ksh4r5Y8DjXizD0qIWOAg62QtFMnUkWgKqIk3UccPjXtfovBrjcC5/RAhPyilK//cEoYf3kDUQfqiwP/7xfaYNhNeGTZ5+NRWE=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "payment_dispute_created", + "name": "payment_dispute_created", + "body": { + "alert_id": "649721479", + "alert_name": "payment_dispute_created", + "amount": "473.05", + "balance_amount": "452.78", + "balance_currency": "EUR", + "balance_fee": "424.33", + "checkout_id": "9-f4ddb7a7b26753e-34bf2cbd3d", + "currency": "USD", + "custom_data": "custom_data", + "email": "roberts.rashawn@example.com", + "event_time": "2023-03-08 09:33:29", + "fee_usd": "0.46", + "marketing_consent": "1", + "order_id": "2", + "passthrough": "Example String", + "status": "open", + "p_signature": "lBTh/VODf1bK7lyyLFeZzGv0MaLsjV8iSN5gT/KFktMfAbRnaVqzfxteXYDotxYRnJj7h1mJ4KvtAtEjQv9ce31G7F0ywvUaQjUUz5KR3SzXGN0E+BKb7jI57+m4au6YVX+DepEUlEig34iEMkow4bAhSNhbvxu5WV4PxK9s1WhLr8FnmHPhwY+jLEb3HYUXtvw0vZPnjmh7Szm3LlonCL0yriokOYVzhvlrVUeMXqfWSxzfeR0pivGJFZn6xUjvj2kB/bFLjAssGZfFOAr0IRqVStNx7s2WB6UGGLex8/tCxawQ4cNo1MeXvrO4SFlUUy12KxZZGt2jgMbBBJCrXlUFoCZYctX+2/1JH0mialpS0R69uQSOJTAHTaDPOvkxPvSrycPcdS5IF3R+1dpM5ATDaPukSolKkWJ6Z60W6AEVKWEXIe1HS8tx/v+YlGKA0UNrk4gTxNjIQOLjYu/nxQoKYwDEge5g5bI347vzH+4uAgEPpLwRadct7fGIPnPiSkYgiACO5estaelG8uhuggfoagGLzTFcWzF7T5vzl7BbwsHh5JrgqGiw5zFhwoL12XRvX64XoVGPkEvxwQBQeXYxud1CwK0my0X1ggFT9a3EdDUd/kqRAg+95UaLMC85CxpJIpij6qeiEfDUBZqUD/ICEStae17vXFl9LYmVFR8=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "payment_refunded", + "name": "payment_refunded", + "body": { + "alert_id": "1873493642", + "alert_name": "payment_refunded", + "amount": "950.51", + "balance_currency": "GBP", + "balance_earnings_decrease": "0.97", + "balance_fee_refund": "0.94", + "balance_gross_refund": "0.48", + "balance_tax_refund": "0.18", + "checkout_id": "5-53dfe7eaa37a4c3-8009830048", + "currency": "USD", + "custom_data": "custom_data", + "earnings_decrease": "0.36", + "email": "kenyatta.goyette@example.org", + "event_time": "2023-03-08 09:33:02", + "fee_refund": "0.21", + "gross_refund": "0.81", + "marketing_consent": "", + "order_id": "3", + "passthrough": "Example String", + "quantity": "97", + "refund_reason": "refund_reason", + "refund_type": "vat", + "tax_refund": "0.71", + "p_signature": "b7UedYpNmqIUsqMDB/3jeOJ1jaXWrnxQPBci51xBNtoaN+jUcu2pD419yk3cijlwv0fpWrr/rJaiszPTFUS2jn0ILvoAf9x5YO22STPp8QeI57iRa8EPOaBaAAC6gk9j4/SUjpIMPUZnaQRQ6OvA9krmJJB2pqDC1JIidn6XBJvQT1/XQ8fj/9HJ9DqM0HwuSMLiApyMrLOwnCijuAowC53TFulj7A8pGsBrtWe/BajHCG25XCPPKKfvzw2pP2/pQcAz/IxO19WII8Z39Iv8hA0QU7482oGPhGuIPj2T0O2NMyUxCbD51e2+5aFM5pBrUJpRtAppcbG0f/6o7A44A/EqpKz1aPtkvGhQxgTXIFRXoreeqzQAd8QmWB7O/0lUP8ITU2Gq30Sr5OXbrEuRRO4dk+mVMUQY+vmba5acce5oqdpxMeL9zPoE6xX5nCppoIy0MSmGMAMrMtoPTrqlq95jVG28hdltBUeR8n3qGrXk1sAw9PGgrKqxOK2ADFXYMwGkr9qoQ/OCj0zF0dWL1+rfHbUVPIJ9XpJezDRmzYXckwWOtBmVis7tkKQ11twl4LAjux48oXuKGkQB5cD3k1cfnrDd2yKxZHLRGWhEat1F3lrRbD6N3ovg4LWdhuuJamceysWdJ7NTfJhaj7xaossrxmLsUOmkVwgbN6bVLUw=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "payment_succeeded", + "name": "payment_succeeded", + "body": { + "alert_id": "1367177662", + "alert_name": "payment_succeeded", + "balance_currency": "GBP", + "balance_earnings": "344.14", + "balance_fee": "779.32", + "balance_gross": "981.69", + "balance_tax": "431.73", + "checkout_id": "7-9540155b027c1b9-20dddece1e", + "country": "GB", + "coupon": "Coupon 9", + "currency": "GBP", + "custom_data": "custom_data", + "customer_name": "customer_name", + "earnings": "29", + "email": "gschimmel@example.org", + "event_time": "2023-03-08 09:32:47", + "fee": "0.59", + "ip": "23.220.176.0", + "marketing_consent": "", + "order_id": "4", + "passthrough": "Example String", + "payment_method": "card", + "payment_tax": "0.21", + "product_id": "1", + "product_name": "Example String", + "quantity": "53", + "receipt_url": "https://sandbox-my.paddle.com/receipt/4/7095047bfd65d79-0244b6e48e", + "sale_gross": "912.59", + "used_price_override": "", + "p_signature": "YIyoeN7qz8JbgLZv9yFcYNjtob0zz+WRrBypgmesHPXtqGpJzYBZit4cCQ/259D1vArAGoGqytf4a17ePOuJBYSfZJXwNVxbOTYFBVMu7J38VeCMu3XhdIBTXopHIpbfnrXrP953J3dUOdpdJ/DvQLsA4Dg9FFld6Eapg6o9RXNHE910HgBjTDY8UJnt7lHTU3IQiyCKejhC8EHKrM6PV1KMTbUmgte71jT+tBBJvILb7JBxSOokYbH8as9lsJUho9HnI1lcxoVj0p6Osyw2Vq8NnMoPNaOFH3hDp4HQlRqXVbRyePOtP6OL78g02W2KHYJNGdXJ1XJ9qhnwrk+jo3whl5p8kwlSK02sSFQSniCCGNDnon18WmXgluGN3xlzpU4R3unueEbice8pL6FBKbYePvCEHVoBno0PubTdu1YOEWxosuBQtXxiuhtGGW6xhMHlPsjceSBkAoVJmCkpg5Ft9hpUfxsMGgTqV9WS3H4FGdzrd+hVuYcalAye2pS0rPMKUqxo6HQD8W+bFpj3/hhyDhPyQ4Qi0DOgvyvkokvwHHzg01tXlJoquLWGfNs8u22dEwG3T7hPEN1wEINkcBvmiXCi+yr02r1JUawBsm02qhZWiuhRCkw0HHJOU5HEeUo66ofu1lJc3DH/7ultO8MGhaL5FOepwjR2CxEvNpk=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "subscription_created", + "name": "subscription_created", + "body": { + "alert_id": "485191415", + "alert_name": "subscription_created", + "cancel_url": "https://sandbox-checkout.paddle.com/subscription/cancel?user=6&subscription=9&hash=d3c456f38ed984f0c2fdd2558d02fbf78ab02a09", + "checkout_id": "6-3d5b947be235b14-9940a81df2", + "currency": "GBP", + "custom_data": "custom_data", + "email": "mann.meggie@example.com", + "event_time": "2023-03-08 09:34:33", + "linked_subscriptions": "5, 6, 3", + "marketing_consent": "1", + "next_bill_date": "2023-03-19", + "passthrough": "Example String", + "quantity": "47", + "source": "Trial", + "status": "active", + "subscription_id": "8", + "subscription_plan_id": "1", + "unit_price": "unit_price", + "update_url": "https://sandbox-checkout.paddle.com/subscription/update?user=6&subscription=3&hash=4a69274867e85e38ede73bee41f4ab98d0734b38", + "user_id": "3", + "p_signature": "JFhLdx5pUvZbBzIh2xxOR0E6OiBNCAkoeGpDNhy+Ji1hiXz/4RyJsKr6R9zSR1qNYWv0rtCpzmclY6knu+EcVQyY/napZ2Jgu2leKhzbMW3087T2WIWXODbIH4n5oJQj8tdAe3olyO7C6AoRCGIR7vKK8OQkgeAGfsdnTyc37G4va6WjNlDBTNcuuxKsRriHPg6AOOFpWwsUAhaPSmRQdGYzi3zu5N+1zgSOn7eYZJ1rF4xf4A0P60BsmQutZ82cdiSVsYNo8ErNSnC+1SgLRKp84oyhl5wcUZRqLwqbApOQv+3o6FsdGP1aRBGUaMcJG9P+svVCwGMdS7wM+DknFQFwlvxaYevMADRgObnaDbERWyjtiOPB0ZCkSfy/OW+ujZa4DZodFaSc6QYfRzFjiuMyGLg91GjxtAfoSVlZ8y5TpSe462pqZCbc0uC3JarIrdqWvMKwufp4TijKzPg60BASr27Ou0B6eGFxL+gUCAEQOoImU1bXz5XsKYumAEaGXzMFuMnIyKNkre07QoJeQcsDzAuBWUeqhGy/7Tz2TJrhtB6js14hzs0fQkw2d1qPwp20kViEiSNQE1dC1W/LTrj45qE3NaP8GeF9lt8vLDFAWRj2kVVJf/wJS2NPyTzi0S136SYafQmkeR2JwCIUb3CyfVZ1rnPji2FhPK54go8=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "subscription_updated", + "name": "subscription_updated", + "body": { + "alert_id": "1353619852", + "alert_name": "subscription_updated", + "cancel_url": "https://sandbox-checkout.paddle.com/subscription/cancel?user=4&subscription=3&hash=a169da876ee69cac5162a66ac63aceb786df1396", + "checkout_id": "3-d01f3e81f7de2f9-550b417f5a", + "currency": "USD", + "custom_data": "custom_data", + "email": "tzieme@example.org", + "event_time": "2023-03-08 09:34:55", + "linked_subscriptions": "3, 4, 8", + "marketing_consent": "1", + "new_price": "new_price", + "new_quantity": "new_quantity", + "new_unit_price": "new_unit_price", + "next_bill_date": "2023-04-01", + "old_next_bill_date": "old_next_bill_date", + "old_price": "old_price", + "old_quantity": "old_quantity", + "old_status": "old_status", + "old_subscription_plan_id": "old_subscription_plan_id", + "old_unit_price": "old_unit_price", + "passthrough": "Example String", + "status": "active", + "subscription_id": "6", + "subscription_plan_id": "7", + "update_url": "https://sandbox-checkout.paddle.com/subscription/update?user=8&subscription=1&hash=9f34f16c64700116c2a0916a40e2646f6c70d103", + "user_id": "8", + "p_signature": "PGvjvycOAxdR77IDY/17wIN0Zy2sLZS+qLI/SjFFsGIY2OkRC/BSaDS2525O1AXZMNmJIz7TDQTL0laxySm4fSgZiwkq0fP2w14NUs+TkoMnxfaGpMG/VGVnzrfhOm3qK2+iiSVSzj0EeT1UQyQd51p0Ud/EftDPZE3Gk8Nyu1hKBN2VZyzeilNwoQy1DcJFrVfIkY4FFl4JwIeAVhr36F8yGOBRnOKTStopueKq11sq+B3pFgPTpPmqCrMkB4fM/+E5HEEqhvhAclh/7NbvHZ0Fd5o1x/67GUV+u20A3t6ufmNZkxu1qboZ49fuZiOdFNFd4Mlpq5Mw7ZogUnGVoT66Pri/VnYztzwzxiC5ZcVj15S/TCKAbH8GI2zN3C8KMM0Xq5bY2Al5rt8I49qE5E5VSS1KUs7gqVJP+AcaftP/xQID5g3FhqpYJmGi4np9tgKC95oVsmYlkZUcjdgud+mY9AHQLhfDhnst52pip19tASwhsDVaBV11YRWeRBxbMZ/zuniFFVqcdAFrXwZhMDZl4lEXBqH30Hizx3b+MVKUHMsQMmv1MqxmVyNd0SIYPcbm5jiw0tTv1fhZsGrI1VU9GnaqdNffbOc7Eik98cc8KBmQJSsCgNVihnW4K2hkOD6+0jpLeT4JwNx4y901yWpD1aodX0zqCkeNJIYKF9Q=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "transfer_created", + "name": "transfer_created", + "body": { + "alert_id": "853473778", + "alert_name": "transfer_created", + "amount": "497.14", + "currency": "EUR", + "event_time": "2023-03-08 09:34:07", + "payout_id": "5", + "status": "unpaid", + "p_signature": "mmrkP43GZW0VzLHhS//oBwxExqJlBU97+QcFCm8DQEl3okYjqrbPCLgIeSwBjhfrQafB4oHEVoLpDf5TOStdUs1ZVhiJdqi3Wz+ftakYsmTSbSiQVy17RqYWBxlD2yn77SdKCGgx51TELiKLrSimxSxo0ypfozAjfhva3Oq8h2rRuGkUudDVIXk1/3yJYJO7xqjuAp0VONOstLNbJspV1uQ1Rq79jNnqmGY+7CEJlbA6MdF2M/8C2FjCD/rQEg/IsQYQDgiGKc0GmDHYcbglIOXIVG8mJctIPCzG+OS8dsGR22Fn8hHtCS59dc8xAtXrTUwlAFySueQqQgs4Uk87tyToqjwjpzwiUCw0s+Vs2bklOrnyQiet+XDIUctz4BWjg6am9ar+Pjb+XktwjwCM2sgGbPt6714j70OEhiNbR7r9c2Fo+sjTM/Br3vMW8q1Q+zzJ4Akfoo2tFCf4FcL82p3217/ZEhDCd/peg3bHake6QfqHlVDxMGOXWE8iYWpNkrC110VVnjwhKE8Ap/Xh3jVYurS7tTAQMosmD3UlLMjls4ydJWTvQJHGeqoGw/b3BU+OrzMXsWAwP+Q4uV03+T9pwxED50a/lh8dvkVitOUSgZxeGjKA+wtkqPmNBOldss3/TG5FjebzZwn3rp4BnHaFS2nG24ax5PoR4DK9X54=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paddleclassic", + "providerLabel": "Paddle Classic", + "eventType": "transfer_paid", + "name": "transfer_paid", + "body": { + "alert_id": "839474484", + "alert_name": "transfer_paid", + "amount": "737.03", + "currency": "EUR", + "event_time": "2023-03-08 09:34:20", + "payout_id": "7", + "status": "paid", + "p_signature": "SjONPgXtEkNg+6Nm6H9XZ6ayEKRUcRpNdfFn3JsGR/fbJu3/jNOKbOXeNGyuy07YwaQtuI1VGu2pBvqnHFQTYNXhr63xx9XnAuYy386+6EYit2NQ1hZt0C0mvfk0gEuByIeUQPYkzxUC6OB9vcjKYn3F6O9/e8Wz8CuM5U6bntheE2iXKKNum15rHPEikaPzZhEBObBWvUAZRflWdYzCqqpcysnkMJrOHWDaJFy0PQzL4sE0unlYentyq7MwXdY+KIY8u5yAJ1YCOp+IvCZJIvUnmiFiHEvhRVwEXC+KMpVGEZ6Fv+O8vyY9Xn4QU87arS6425RKbr4gZLxZ6RFnemSqU8+ZabpLgCjswKASsUP4DVwqitKZejo09AL+Ck3ubQ7aUrl4SzpgVvBe7h411XelQPey1yY89Dkd6S222h4TMwf5xxGS/owcfN8u6QSqVoBShICTc4uQw8ju+J7QMES1MvMMGQ14xhv5o6OA+vy5GuHvOzSB4JC6MYfQneBOq+FiU+WwDiZ1LAnluSdIRI4hl8HMWQQYW94g6X9eJFSZ7pVisXUxijDRQR7vD+sYvzCHDBiEiAbTaPB6pxiO/WDtC7omX06qKOEMIEEWsCt0VwUrj3uk5s6t8AZ9y4zJWM1vOERC9cpntyZkdV2l5pfHZqdF09f7zeWL0Q6DrWY=" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paddleclassic/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.PLAN.ACTIVATED", + "name": "BILLING.PLAN.ACTIVATED", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R661687G", + "create_time": "2018-19-12T22:20:32.000Z", + "event_type": "BILLING.PLAN.ACTIVATED", + "event_version": "1.0", + "resource_type": "plan", + "resource_version": "2.0", + "summary": "A billing plan was activated.", + "resource": { + "id": "P-7GL4271244454362WXNWU5NQ", + "product_id": "PROD-XXCD1234QWER65782", + "name": "Zoho Marketing Campaign Plan", + "description": "Zoho Marketing Campaign Plan", + "status": "ACTIVE", + "usage_type": "LICENSED", + "billing_cycles": [ + { + "frequency": { + "interval_unit": "MONTH", + "interval_count": 1 + }, + "tenure_type": "TRIAL", + "sequence": 1, + "total_cycles": 1, + "pricing_scheme": { + "fixed_price": { + "value": "50", + "currency_code": "USD" + }, + "tier_mode": "VOLUME", + "tiers": [ + { + "starting_quantity": "1", + "ending_quantity": "1000", + "amount": { + "value": "100", + "currency_code": "USD" + } + }, + { + "starting_quantity": "1001", + "amount": { + "value": "200", + "currency_code": "USD" + } + } + ] + } + }, + { + "frequency": { + "interval_unit": "MONTH", + "interval_count": 1 + }, + "tenure_type": "REGULAR", + "sequence": 2, + "total_cycles": 12, + "pricing_scheme": { + "fixed_price": { + "value": "100", + "currency_code": "USD" + }, + "tier_mode": "VOLUME", + "tiers": [ + { + "starting_quantity": "1", + "ending_quantity": "1000", + "amount": { + "value": "300", + "currency_code": "USD" + } + }, + { + "starting_quantity": "1001", + "amount": { + "value": "1000", + "currency_code": "USD" + } + } + ] + } + } + ], + "payment_preferences": { + "service_type": "PREPAID", + "auto_bill_outstanding": true, + "setup_fee": { + "value": "10", + "currency_code": "USD" + }, + "setup_fee_failure_action": "CONTINUE", + "payment_failure_threshold": 3 + }, + "taxes": { + "percentage": "10", + "inclusive": false + }, + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ", + "rel": "edit", + "method": "PATCH" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e0b2fe*Host=ccg14amqunphttpdeliveryd57", + "client_pid": "763685", + "correlation-id": "8304c2d1f9b20", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "e068dce0-ba1d-11ed-8635-fb7d730e54d3", + "paypal-transmission-sig": "fblPC+IWjjdLepncpX86WCHQij9XMQaA7zmgA62vmoc7DIR4ThIBIkZ349YetJFKQuBKi26uBJeKD7RroEC2UQ1KzIVUdZVwUTDfkyIk+lRzYjz0Tau0vgQHKHovZ1aiAKTA96jb2kPW5zATHBZzP30Yhca2ICcNCA2a71fIzITVdgK/q4yJkvbcnfdWu3CHi1vnKA+X0rXQUIH9O8+Bo51cSdXBBIWvvsd5ghqK1N2Ex0yC4H4R/2PQNOnNJpzXseaJzcmoumaH5LbtaOfX6WvJB6xS5cApNPQ46l7+anfRD72fDcon/bZA4vn25JtMOeY+j3eucOhdUgkwekQZWw==", + "paypal-transmission-time": "2023-03-03T23:48:20Z", + "x-b3-spanid": "2ae3db9ba6f04589" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.PLAN.CREATED", + "name": "BILLING.PLAN.CREATED", + "body": { + "id": "WH-2N1678257S892762B-8MC99539P4557624Y", + "event_version": "1.0", + "create_time": "2016-04-28T11:21:44Z", + "resource_type": "plan", + "event_type": "BILLING.PLAN.CREATED", + "summary": "A billing plan was created", + "resource": { + "id": "P-7LT50814996943336LSNDODY", + "merchant_preferences": { + "setup_fee": { + "currency": "USD", + "value": "1" + }, + "return_url": "http://www.paypal.com", + "cancel_url": "http://www.yahoo.com", + "auto_bill_amount": "YES", + "initial_fail_amount_action": "CONTINUE", + "max_fail_attempts": "21" + }, + "update_time": "2016-04-28T11:21:31.151Z", + "description": "test web new", + "payment_definitions": [ + { + "name": "Payment Definition-1", + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "currency": "USD", + "value": "10" + }, + "cycles": "15", + "charge_models": [ + { + "type": "SHIPPING", + "amount": { + "currency": "USD", + "value": "1" + }, + "id": "CHM-29V063578B632154XLSNDODY" + }, + { + "type": "TAX", + "amount": { + "currency": "USD", + "value": "2" + }, + "id": "CHM-72N78855RJ303084YLSNDODY" + } + ], + "id": "PD-28U66480VB469201XLSNDODY" + }, + { + "name": "Payment Definition-2", + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "currency": "USD", + "value": "5" + }, + "cycles": "5", + "charge_models": [ + { + "type": "SHIPPING", + "amount": { + "currency": "USD", + "value": "1" + }, + "id": "CHM-4CT119433N5199501LSNDODY" + }, + { + "type": "TAX", + "amount": { + "currency": "USD", + "value": "1" + }, + "id": "CHM-38H015979N656741TLSNDODY" + } + ], + "id": "PD-99B78670BE878604GLSNDODY" + } + ], + "name": "Fast Speed Plan", + "state": "CREATED", + "create_time": "2016-04-28T11:21:31.151Z", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-plans/P-7LT50814996943336LSNDODY", + "rel": "self", + "method": "GET" + } + ], + "type": "FIXED" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-2N1678257S892762B-8MC99539P4557624Y", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-2N1678257S892762B-8MC99539P4557624Y/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e1d6de*Host=ccg13amqunphttpdeliveryd34", + "client_pid": "652346", + "correlation-id": "2a5ee4870ad28", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "01056770-ba1e-11ed-82f7-c746d6224898", + "paypal-transmission-sig": "lCGXoAD9A/6EJ3cb15OOI8VLq8FFx37z9k6/cbzUR4Ll3UOGPcIYRm/4YVhqnIKA25XFHhYsyaPDimOLKr1JvImzASUe+Dod0X9KKAguEp7AAQI0nxnAlp3IEcSiRNCH6W9+MEEseZp2+ILLwiyZsx06vyUCViVwU6HKtfInj9xH9g/pjoI9X1wARVjwoHpbJrUGzCbQUwdZKfdQkGNKmoIso6xP1nlnUlX8VJFbSlHFvuF/eMVtBVx+XqVPqWo5Rj8leZwOmrFXLAWTsrKnolGm5SOQx8IB+5eMeln3hZyjTuDmcE/KBjvJ4/2eipX48xqAXsc1PRrTkLOArfbXxA==", + "paypal-transmission-time": "2023-03-03T23:49:14Z", + "x-b3-spanid": "328a9ad597f97a41" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.PLAN.PRICING-CHANGE.ACTIVATED", + "name": "BILLING.PLAN.PRICING-CHANGE.ACTIVATED", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R771687G", + "create_time": "2018-19-12T22:20:32.000Z", + "event_type": "BILLING.PLAN.PRICING-CHANGE.ACTIVATED", + "event_version": "1.0", + "resource_type": "plan", + "resource_version": "2.0", + "summary": "A billing plan pricing change is activated.", + "resource": { + "id": "P-7GL4271244454362WXNWU5NQ", + "product_id": "PROD-XXCD1234QWER65782", + "name": "Zoho Marketing Campaign Plan", + "description": "Zoho Marketing Campaign Plan", + "status": "ACTIVE", + "usage_type": "LICENSED", + "billing_cycles": [ + { + "frequency": { + "interval_unit": "MONTH", + "interval_count": 1 + }, + "tenure_type": "TRIAL", + "sequence": 1, + "total_cycles": 1, + "pricing_scheme": { + "status": "ACTIVE", + "version": 2, + "fixed_price": { + "value": "50", + "currency_code": "USD" + }, + "tier_mode": "VOLUME", + "tiers": [ + { + "starting_quantity": "1", + "ending_quantity": "1000", + "amount": { + "value": "100", + "currency_code": "USD" + } + }, + { + "starting_quantity": "1001", + "amount": { + "value": "200", + "currency_code": "USD" + } + } + ] + } + }, + { + "frequency": { + "interval_unit": "MONTH", + "interval_count": 1 + }, + "tenure_type": "REGULAR", + "sequence": 2, + "total_cycles": 12, + "pricing_scheme": { + "status": "ACTIVE", + "version": 1, + "fixed_price": { + "value": "100", + "currency_code": "USD" + }, + "tier_mode": "VOLUME", + "tiers": [ + { + "starting_quantity": "1", + "ending_quantity": "1000", + "amount": { + "value": "300", + "currency_code": "USD" + } + }, + { + "starting_quantity": "1001", + "amount": { + "value": "1000", + "currency_code": "USD" + } + } + ] + } + } + ], + "payment_preferences": { + "service_type": "PREPAID", + "auto_bill_outstanding": true, + "setup_fee": { + "value": "10", + "currency_code": "USD" + }, + "setup_fee_failure_action": "CONTINUE", + "payment_failure_threshold": 3 + }, + "taxes": { + "percentage": "10", + "inclusive": false + }, + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ", + "rel": "edit", + "method": "PATCH" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R771687G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R771687G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e3122e*Host=ccg14amqunphttpdeliveryd1", + "client_pid": "763718", + "correlation-id": "16aa635264a86", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "267dfe90-ba1e-11ed-b9b7-69139b55be60", + "paypal-transmission-sig": "ioUvHMa+otPQM4IQBih6+eFuvo+mju0ncPQ6SKaCPd3aapiDitUJA8DHuVFXXnY+3b4ZmZyvPj4kV0Tvvp1LVcQuGz72BZql9FmoRoZlEeNFby11t0DjnzDkZN8nb//FN1xNA3XGwTlN6FT5dmNhY+gv+K17PUBrdbIpMN+Plkq2hirJSSUIWTMWORQFC6OhGsUYF53xO2f7oMTDozPgJjU0vuYpZiFnyGJAWlSSbspfd2aWvrIsUfW7D1unuorMOhA3SsNJ2lKlu3+lIK02qSYUnq0ZDY1mYQU7sWlyciLGBfLRTW/2kPV+Lhqt/+RJfVOlfBGpGN5ECXoBUlgCXg==", + "paypal-transmission-time": "2023-03-03T23:50:17Z", + "x-b3-spanid": "18c4d85396c02cbf" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.PLAN.PRICING-CHANGE.INPROGRESS", + "name": "BILLING.PLAN.PRICING-CHANGE.INPROGRESS", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R672787G", + "create_time": "2018-19-12T22:20:32.000Z", + "event_type": "BILLING.PLAN.PRICING-CHANGE.INPROGRESS", + "event_version": "1.0", + "resource_type": "plan", + "resource_version": "2.0", + "summary": "A billing plan pricing change is being processed.", + "resource": { + "id": "P-7GL4271244454362WXNWU5NQ", + "product_id": "PROD-XXCD1234QWER65782", + "name": "Zoho Marketing Campaign Plan", + "description": "Zoho Marketing Campaign Plan", + "status": "ACTIVE", + "usage_type": "LICENSED", + "billing_cycles": [ + { + "frequency": { + "interval_unit": "MONTH", + "interval_count": 1 + }, + "tenure_type": "TRIAL", + "sequence": 1, + "total_cycles": 1, + "pricing_scheme": { + "status": "IN_PROGRESS", + "version": 2, + "fixed_price": { + "value": "50", + "currency_code": "USD" + }, + "tier_mode": "VOLUME", + "tiers": [ + { + "starting_quantity": "1", + "ending_quantity": "1000", + "amount": { + "value": "100", + "currency_code": "USD" + } + }, + { + "starting_quantity": "1001", + "amount": { + "value": "200", + "currency_code": "USD" + } + } + ] + } + }, + { + "frequency": { + "interval_unit": "MONTH", + "interval_count": 1 + }, + "tenure_type": "REGULAR", + "sequence": 2, + "total_cycles": 12, + "pricing_scheme": { + "status": "ACTIVE", + "version": 1, + "fixed_price": { + "value": "100", + "currency_code": "USD" + }, + "tier_mode": "VOLUME", + "tiers": [ + { + "starting_quantity": "1", + "ending_quantity": "1000", + "amount": { + "value": "300", + "currency_code": "USD" + } + }, + { + "starting_quantity": "1001", + "amount": { + "value": "1000", + "currency_code": "USD" + } + } + ] + } + } + ], + "payment_preferences": { + "service_type": "PREPAID", + "auto_bill_outstanding": true, + "setup_fee": { + "value": "10", + "currency_code": "USD" + }, + "setup_fee_failure_action": "CONTINUE", + "payment_failure_threshold": 3 + }, + "taxes": { + "percentage": "10", + "inclusive": false + }, + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/billing/plans/P-5ML4271244454362WXNWU5NQ", + "rel": "edit", + "method": "PATCH" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R672787G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R672787G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e2943e*Host=ccg14amqunphttpdeliveryd12", + "client_pid": "764269", + "correlation-id": "d449a01c23c2c", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "29f83130-ba1e-11ed-9594-bb45e37dc329", + "paypal-transmission-sig": "BBtBrVrRGCrMuWaa/hrwp8KQwyVdLjOoQFjyT59tlDr9acxLvYk8ckdFCrzdtsxE23xn+rKE1AfBYyUF6/KWEkignOtN9dEmxFqYdm4zilKOGuiyP2aqBX5Zy3LgLynNBA/Hoxvemn0t8LLUQDxUhkQ0bU39XfrPlRpH5Lc5z+YVsEiEyTZUyT1tG2SHoWVjbgn8kpZDIeTSZwe36CthwwpZUMGh99gzJqNHo24S+fE2m5UynR+m6ERS1RlNh4X3vkSwxLQcYJ5GCU6IrQt3SohSRxoCVIiSq+O99K6UdY+N0F4ZybhdDUQlO9sq9hxON5pBNO6q25X6FeSKNDCx5Q==", + "paypal-transmission-time": "2023-03-03T23:50:23Z", + "x-b3-spanid": "5325233325a00fb5" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.PLAN.UPDATED", + "name": "BILLING.PLAN.UPDATED", + "body": { + "id": "WH-303507964T970970H-60Y688551M4794625", + "event_version": "1.0", + "create_time": "2016-04-28T11:23:52Z", + "resource_type": "plan", + "event_type": "BILLING.PLAN.UPDATED", + "summary": "A billing plan was updated", + "resource": { + "id": "P-7LT50814996943336LSNDODY", + "merchant_preferences": { + "setup_fee": { + "currency": "USD", + "value": "1" + }, + "return_url": "http://www.paypal.com", + "cancel_url": "http://www.yahoo.com", + "auto_bill_amount": "YES", + "initial_fail_amount_action": "CONTINUE", + "max_fail_attempts": "21" + }, + "update_time": "2016-04-28T11:23:27.568Z", + "description": "test web new", + "payment_definitions": [ + { + "name": "Payment Definition-1", + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "currency": "USD", + "value": "10" + }, + "cycles": "15", + "charge_models": [ + { + "type": "TAX", + "amount": { + "currency": "USD", + "value": "2" + }, + "id": "CHM-72N78855RJ303084YLSNDODY" + }, + { + "type": "SHIPPING", + "amount": { + "currency": "USD", + "value": "1" + }, + "id": "CHM-29V063578B632154XLSNDODY" + } + ], + "id": "PD-28U66480VB469201XLSNDODY" + }, + { + "name": "Payment Definition-2", + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "currency": "USD", + "value": "5" + }, + "cycles": "5", + "charge_models": [ + { + "type": "TAX", + "amount": { + "currency": "USD", + "value": "1" + }, + "id": "CHM-38H015979N656741TLSNDODY" + }, + { + "type": "SHIPPING", + "amount": { + "currency": "USD", + "value": "1" + }, + "id": "CHM-4CT119433N5199501LSNDODY" + } + ], + "id": "PD-99B78670BE878604GLSNDODY" + } + ], + "name": "Fast Speed Plan", + "state": "ACTIVE", + "create_time": "2016-04-28T11:21:31.151Z", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-plans/P-7LT50814996943336LSNDODY", + "rel": "self", + "method": "GET" + } + ], + "type": "FIXED" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-303507964T970970H-60Y688551M4794625", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-303507964T970970H-60Y688551M4794625/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e2add4*Host=ccg13amqunphttpdeliveryd3", + "client_pid": "715815", + "correlation-id": "183c29e59f2cf", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "2e124d50-ba1e-11ed-b43e-bdc2007fb7f3", + "paypal-transmission-sig": "ZDHvipBk/ybAXn3qprOldo8FKdTGB1C2U9UMZ/0/sLNydW9RIYzI+SwhoHS13jeM+LsUsyVdv5J7lQg1B1MqGm6jfEsGVTNOMW8okvPkw4c09qHds7XojOitYcjvEDjjryBNb8id+nawqvctLhkya3AxDT3EEpp7L57cjhrr+wLS/3ocqHpIsknWgx1IW/mSl/HnQRFG1hKiANV4SN8eWBN5nFwsP4UaJHTkuPq61osxOUwdnFvwPpWwZzPjdg3i59X5i6k6ID2H0UE5WiFwrcfRMrJD85zDPR7z3oMLQiqjmc/Wq2Lb7fvXDsAr18ks98nZmzUnDmHmPSfZOhUV1g==", + "paypal-transmission-time": "2023-03-03T23:50:30Z", + "x-b3-spanid": "81157169586502d4" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.ACTIVATED", + "name": "BILLING.SUBSCRIPTION.ACTIVATED", + "body": { + "id": "WH-77687562XN25889J8-8Y6T55435R66168T6", + "create_time": "2018-19-12T22:20:32.000Z", + "event_type": "BILLING.SUBSCRIPTION.ACTIVATED", + "event_version": "1.0", + "resource_type": "subscription", + "resource_version": "2.0", + "summary": "A billing agreement was activated.", + "resource": { + "id": "I-BW452GLLEP1G", + "status": "ACTIVE", + "status_update_time": "2018-12-10T21:20:49Z", + "plan_id": "P-5ML4271244454362WXNWU5NQ", + "start_time": "2018-11-01T00:00:00Z", + "quantity": "20", + "shipping_amount": { + "currency_code": "USD", + "value": "10.00" + }, + "subscriber": { + "name": { + "given_name": "John", + "surname": "Doe" + }, + "email_address": "customer@example.com", + "shipping_address": { + "name": { + "full_name": "John Doe" + }, + "address": { + "address_line_1": "2211 N First Street", + "address_line_2": "Building 17", + "admin_area_2": "San Jose", + "admin_area_1": "CA", + "postal_code": "95131", + "country_code": "US" + } + } + }, + "auto_renewal": true, + "billing_info": { + "outstanding_balance": { + "currency_code": "USD", + "value": "10.00" + }, + "cycle_executions": [ + { + "tenure_type": "TRIAL", + "sequence": 1, + "cycles_completed": 1, + "cycles_remaining": 0, + "current_pricing_scheme_version": 1 + }, + { + "tenure_type": "REGULAR", + "sequence": 2, + "cycles_completed": 1, + "cycles_remaining": 0, + "current_pricing_scheme_version": 2 + } + ], + "last_payment": { + "amount": { + "currency_code": "USD", + "value": "500.00" + }, + "time": "2018-12-01T01:20:49Z" + }, + "next_billing_time": "2019-01-01T00:20:49Z", + "final_payment_time": "2020-01-01T00:20:49Z", + "failed_payments_count": 2 + }, + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G", + "rel": "edit", + "method": "PATCH" + }, + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/suspend", + "rel": "suspend", + "method": "POST" + }, + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/cancel", + "rel": "cancel", + "method": "POST" + }, + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/capture", + "rel": "capture", + "method": "POST" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e38132*Host=ccg13amqunphttpdeliveryd54", + "client_pid": "768928", + "correlation-id": "cc7cb2973ec9f", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "395db0a0-ba1e-11ed-9552-9babbd01d13a", + "paypal-transmission-sig": "Rmr5qG7LFk7G0GoI1sm7OT9w+cWG+RddBQEJo17jiEaVbFR7/WYzYSjgfSYup3phXKwb1y5LVTYR5OrhA879AVgxrW3llRGdf9Iq2q8/m3tQgpE1bnkmDHFpVm0Z028WXAxUEhk1rlNUfciJaEKv+yfSMSB/PVHdR+OwCLN1rMgPFfgvaH529fD07Zu9MqK6m0BePaxoII8O+9bb1wU+GC02PPkoAp6aVVaTqTilfy12lQOHz89NtNXKjebUjjxqpTksqEkLSWFaDYqwL21ns6nwqPIN/n6aI8a7Z+rG4QZM9a0oa94r+1iuK6rSflD8oYxlzi4ZX1uXOoff8GseWA==", + "paypal-transmission-time": "2023-03-03T23:50:49Z", + "x-b3-spanid": "3e987183e4807b4e" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.CANCELLED", + "name": "BILLING.SUBSCRIPTION.CANCELLED", + "body": { + "id": "WH-6TD369808N914414D-1YJ376786E892292F", + "event_version": "1.0", + "create_time": "2016-04-28T11:53:10Z", + "resource_type": "Agreement", + "event_type": "BILLING.SUBSCRIPTION.CANCELLED", + "summary": "A billing subscription was cancelled", + "resource": { + "id": "I-PE7JWXKGVN0R", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + }, + "plan": { + "curr_code": "USD", + "links": [], + "payment_definitions": [ + { + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "5.00" + }, + "cycles": "5", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "1.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + }, + { + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "10.00" + }, + "cycles": "15", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "2.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + } + ], + "merchant_preferences": { + "setup_fee": { + "value": "0.00" + }, + "auto_bill_amount": "YES", + "max_fail_attempts": "21" + } + }, + "payer": { + "payment_method": "paypal", + "status": "verified", + "payer_info": { + "email": "coolbuyer@example.com", + "first_name": "Cool", + "last_name": "Buyer", + "payer_id": "XLHKRXRA4H7QY", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + } + } + }, + "agreement_details": { + "outstanding_balance": { + "value": "0.00" + }, + "num_cycles_remaining": "5", + "num_cycles_completed": "0", + "last_payment_date": "2016-04-28T11:29:54Z", + "last_payment_amount": { + "value": "1.00" + }, + "final_payment_due_date": "2017-11-30T10:00:00Z", + "failed_payment_count": "0" + }, + "description": "update desc", + "state": "Cancelled", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-agreements/I-PE7JWXKGVN0R", + "rel": "self", + "method": "GET" + } + ], + "start_date": "2016-04-30T07:00:00Z" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-6TD369808N914414D-1YJ376786E892292F", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-6TD369808N914414D-1YJ376786E892292F/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e35482*Host=ccg14amqunphttpdeliveryd21", + "client_pid": "765121", + "correlation-id": "b81ec97195256", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "3f983bc0-ba1e-11ed-b701-5da37665886d", + "paypal-transmission-sig": "XHEBcsYzyNtXXsdfGHdg72tu7OzOMRk10MdR5FO1EcAdn99LdBozRRrZzr4o7GQBQrmaDvxTfaSAnvtaZE5ccMt1wTNy0Hb9mlilqbFMDaiuzM9XYM+WgOIahLN8JrEPUyd4U60mKKAAWMAueZukFF4tTYeJgWLPsd4hidZN7tji0lDWQIwLdfTotGQ1lwAw9b9WBZrkj+kIU42nE1eGhchYkAJ0AmISRAYaA4dSHw1nFQQ6wqs4iU5p/Amv1u1KHXPqmfMBhJYwASe880OGfA1Pz/rGYyh0aMuc37rq4idzAaQHLYFYvjZ8MH/KJdB+QE+0D32qDiVQFu1yiF+TVA==", + "paypal-transmission-time": "2023-03-03T23:50:59Z", + "x-b3-spanid": "106851feec412bfd" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.CREATED", + "name": "BILLING.SUBSCRIPTION.CREATED", + "body": { + "id": "WH-19973937YW279670F-02S63370HL636500Y", + "event_version": "1.0", + "create_time": "2016-04-28T11:29:31Z", + "resource_type": "Agreement", + "event_type": "BILLING.SUBSCRIPTION.CREATED", + "summary": "A billing subscription was created", + "resource": { + "id": "I-PE7JWXKGVN0R", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + }, + "plan": { + "curr_code": "USD", + "links": [], + "payment_definitions": [ + { + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "5.00" + }, + "cycles": "5", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "1.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + }, + { + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "10.00" + }, + "cycles": "15", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "2.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + } + ], + "merchant_preferences": { + "setup_fee": { + "value": "0.00" + }, + "auto_bill_amount": "YES", + "max_fail_attempts": "21" + } + }, + "payer": { + "payment_method": "paypal", + "status": "verified", + "payer_info": { + "email": "coolbuyer@example.com", + "first_name": "Cool", + "last_name": "Buyer", + "payer_id": "XLHKRXRA4H7QY", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + } + } + }, + "agreement_details": { + "outstanding_balance": { + "value": "0.00" + }, + "num_cycles_remaining": "5", + "num_cycles_completed": "0", + "final_payment_due_date": "2017-11-30T10:00:00Z", + "failed_payment_count": "0" + }, + "description": "desc", + "state": "Pending", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-agreements/I-PE7JWXKGVN0R", + "rel": "self", + "method": "GET" + } + ], + "start_date": "2016-04-30T07:00:00Z" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-19973937YW279670F-02S63370HL636500Y", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-19973937YW279670F-02S63370HL636500Y/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e37372*Host=ccg13amqunphttpdeliveryd56", + "client_pid": "708179", + "correlation-id": "17884c792681f", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "4604d360-ba1e-11ed-b26e-4700f011de08", + "paypal-transmission-sig": "UoWA6+seL6TCWr+o8Od2bTSEnNeor/HSbotB7qcYNBXWL6/s/GbS0I91Gg6RUf0ZtHfbW5MlfI8Kzufu9BdUP4fsNxdOLrSk4E/vLj/mNBkvLv1IrZh/ZcOGSM68c4FBlL2mkQCJdjp7iSgxNrAU3Uk4KgtUH14mGxR8zvR9iQeeLSoOA/AH846xwKbylxq0as7gQHfhgqR25rjnOlvESUiJewZx6ZguZMkMQlqCBciAE0R1KdBDgxZ8Rvu+P6/YFgI3HUwmyCUTNaZTNgqQt9VZpbQjWkflb6qctJUTVWLn5wteHhG46eC6ZTZyF8vYyswbqbQqTyFMR2UD5AgqVA==", + "paypal-transmission-time": "2023-03-03T23:51:10Z", + "x-b3-spanid": "f1ee2342940f8fcf" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.EXPIRED", + "name": "BILLING.SUBSCRIPTION.EXPIRED", + "body": { + "id": "WH-UY687577TY25889J9-2R6T55435R66168Y6", + "create_time": "2018-19-12T22:20:32.000Z", + "event_type": "BILLING.SUBSCRIPTION.EXPIRED", + "event_version": "1.0", + "resource_type": "subscription", + "resource_version": "2.0", + "summary": "A billing agreement has expired.", + "resource": { + "id": "I-BW452GLLEP1G", + "status": "EXPIRED", + "status_update_time": "2018-12-10T21:20:49Z", + "plan_id": "P-5ML4271244454362WXNWU5NQ", + "start_time": "2018-11-01T00:00:00Z", + "quantity": "20", + "shipping_amount": { + "currency_code": "USD", + "value": "10.00" + }, + "subscriber": { + "name": { + "given_name": "John", + "surname": "Doe" + }, + "email_address": "customer@example.com", + "shipping_address": { + "name": { + "full_name": "John Doe" + }, + "address": { + "address_line_1": "2211 N First Street", + "address_line_2": "Building 17", + "admin_area_2": "San Jose", + "admin_area_1": "CA", + "postal_code": "95131", + "country_code": "US" + } + } + }, + "auto_renewal": true, + "billing_info": { + "outstanding_balance": { + "currency_code": "USD", + "value": "10.00" + }, + "cycle_executions": [ + { + "tenure_type": "TRIAL", + "sequence": 1, + "cycles_completed": 1, + "cycles_remaining": 0, + "current_pricing_scheme_version": 1 + }, + { + "tenure_type": "REGULAR", + "sequence": 2, + "cycles_completed": 1, + "cycles_remaining": 0, + "current_pricing_scheme_version": 1 + } + ], + "last_payment": { + "amount": { + "currency_code": "USD", + "value": "500.00" + }, + "time": "2018-12-01T01:20:49Z" + }, + "next_billing_time": "2019-01-01T00:20:49Z", + "final_payment_time": "2020-01-01T00:20:49Z", + "failed_payments_count": 2 + }, + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G", + "rel": "self", + "method": "GET" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-UY687577TY25889J9-2R6T55435R66168Y6", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-UY687577TY25889J9-2R6T55435R66168Y6/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e3992e*Host=ccg14amqunphttpdeliveryd14", + "client_pid": "763312", + "correlation-id": "935e2dfa05f35", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "4925c270-ba1e-11ed-89ca-b71bfd0b813f", + "paypal-transmission-sig": "ZDTn0v/sZ3+LweerJ8omVwsnQVWepYVgXxM55pYzEDt8Qcnte7REnfbeHQtFtGtBoEWc/p2ntu3cMD0qnO1SU1ttO08uq9bVnQubdcc8BGYFJLTZbUVumzNSoGmcHoB9nScTY+qF+kBPPQXawzJHIQtIu1xyABoEflGJvFGR7YuKVZr1i0vAhtG1riLfTe6K77WjLVxTmFrBC1JdnZ9IBCcSDUBKyGtEl/DFJev5zyHjXCs7v1+WhQNZGAMJvHOPiHojYWHrtPMEQGsW5tTopTNV9eKgwzoeduFPKgfZrVo12nw4ehm0EaGc0p33cv3siN0f1pzrNAKT1F8cS9RAKg==", + "paypal-transmission-time": "2023-03-03T23:51:15Z", + "x-b3-spanid": "3530d1aa54d7793a" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.PAYMENT.FAILED", + "name": "BILLING.SUBSCRIPTION.PAYMENT.FAILED", + "body": { + "id": "WH-77687562XN25889J2-8Y6T55435R66168T4", + "create_time": "2019-01-01T01:20:54.000Z", + "event_type": "BILLING.SUBSCRIPTION.PAYMENT.FAILED", + "event_version": "1.0", + "resource_type": "subscription", + "resource_version": "2.0", + "summary": "Subscription payment failed", + "resource": { + "id": "I-BW452GLLEP1G", + "status": "ACTIVE", + "status_update_time": "2018-12-10T21:20:49Z", + "plan_id": "P-5ML4271244454362WXNWU5NQ", + "start_time": "2018-11-01T00:00:00Z", + "quantity": "20", + "shipping_amount": { + "currency_code": "USD", + "value": "10.00" + }, + "subscriber": { + "name": { + "given_name": "John", + "surname": "Doe" + }, + "email_address": "customer@example.com", + "payer_id": "2J6QB8YJQSJRJ", + "shipping_address": { + "name": { + "full_name": "John Doe" + }, + "address": { + "address_line_1": "2211 N First Street", + "address_line_2": "Building 17", + "admin_area_2": "San Jose", + "admin_area_1": "CA", + "postal_code": "95131", + "country_code": "US" + } + } + }, + "billing_info": { + "outstanding_balance": { + "currency_code": "USD", + "value": "10.00" + }, + "cycle_executions": [ + { + "tenure_type": "TRIAL", + "sequence": 1, + "cycles_completed": 1, + "cycles_remaining": 0, + "current_pricing_scheme_version": 1 + }, + { + "tenure_type": "REGULAR", + "sequence": 2, + "cycles_completed": 1, + "cycles_remaining": 0, + "current_pricing_scheme_version": 1 + } + ], + "last_payment": { + "amount": { + "currency_code": "USD", + "value": "500.00" + }, + "time": "2018-12-01T01:20:49Z" + }, + "last_failed_payment": { + "amount": { + "currency_code": "USD", + "value": "500.00" + }, + "time": "2019-01-01T01:20:49Z", + "reason_code": "PAYMENT_DENIED", + "next_payment_retry_date": "2019-01-06T01:20:49Z" + }, + "next_billing_time": "2019-01-01T00:20:49Z", + "final_payment_time": "2020-01-01T00:20:49Z", + "failed_payments_count": 2 + }, + "create_time": "2018-10-20T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G?fields=last_failed_payment", + "rel": "self", + "method": "GET" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J2-8Y6T55435R66168T4", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J2-8Y6T55435R66168T4/resend", + "rel": "resend", + "method": "POST" + }, + { + "href": "https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G", + "rel": "subscription", + "method": "GET" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e34230*Host=ccg13amqunphttpdeliveryd37", + "client_pid": "775226", + "correlation-id": "66d2aac69842b", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "4bf7ce30-ba1e-11ed-8c49-d5ed14aef4b1", + "paypal-transmission-sig": "J0zVAJHvfk5kB80TShebVnsi70T18afMB4nT+0pRRlF21aqc/hahC5a8SU1pdB1HSre1FvZtBW2vAekraoqYwd7ur17iyoEBNyT2ofa32wXEDmZeUS7jDH2en8UTLAqbZ+k3DINwWPebgHIgBmP8d2hgbXin7+xYDctQFlFQThMeAshQxJKYGOkTsH7bUrWtcVcgb2gkJJfUYuyYASEGBy9xTWi1pkHt7WmM2OBBxtaGJo8i2eSf8fejU2BI7VLga3bQFKuTdQEmtReZWoWRKyL2hxM93ZEFJ30Slzcrm//GRdEzeGJBEGYuogGodTltFFEdaNeFZdI+JMeXo7VFPQ==", + "paypal-transmission-time": "2023-03-03T23:51:20Z", + "x-b3-spanid": "12258d7a1b42b227" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.RE-ACTIVATED", + "name": "BILLING.SUBSCRIPTION.RE-ACTIVATED", + "body": { + "id": "WH-8KT47529P7696805Y-3CS72897S14937140", + "event_version": "1.0", + "create_time": "2016-04-28T11:41:01Z", + "resource_type": "Agreement", + "event_type": "BILLING.SUBSCRIPTION.RE-ACTIVATED", + "summary": "A billing subscription was reactivated", + "resource": { + "id": "I-PE7JWXKGVN0R", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + }, + "plan": { + "curr_code": "USD", + "links": [], + "payment_definitions": [ + { + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "5.00" + }, + "cycles": "5", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "1.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + }, + { + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "10.00" + }, + "cycles": "15", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "2.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + } + ], + "merchant_preferences": { + "setup_fee": { + "value": "0.00" + }, + "auto_bill_amount": "YES", + "max_fail_attempts": "21" + } + }, + "payer": { + "payment_method": "paypal", + "status": "verified", + "payer_info": { + "email": "coolbuyer@example.com", + "first_name": "Cool", + "last_name": "Buyer", + "payer_id": "XLHKRXRA4H7QY", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + } + } + }, + "agreement_details": { + "outstanding_balance": { + "value": "0.00" + }, + "num_cycles_remaining": "5", + "num_cycles_completed": "0", + "next_billing_date": "2016-04-30T07:00:00Z", + "last_payment_date": "2016-04-28T11:29:54Z", + "last_payment_amount": { + "value": "1.00" + }, + "final_payment_due_date": "2017-11-30T10:00:00Z", + "failed_payment_count": "0" + }, + "description": "desc", + "state": "Active", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-agreements/I-PE7JWXKGVN0R", + "rel": "self", + "method": "GET" + } + ], + "start_date": "2016-04-30T07:00:00Z" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-8KT47529P7696805Y-3CS72897S14937140", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-8KT47529P7696805Y-3CS72897S14937140/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e39366*Host=ccg13amqunphttpdeliveryd26", + "client_pid": "769475", + "correlation-id": "edf62a3dd97ff", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "4efcf7e0-ba1e-11ed-9552-9babbd01d13a", + "paypal-transmission-sig": "nStQQFzoNp4sRtorQQSDfbjj7L4efDHwdcBNpVHAsBnIr7RwwMWayz62QMtF3jhwfJwMfobA6wrs+a8QJzik3s2H8nRqh9SW9jfcu3xWj6bk0VT0qx6lGvg+oVKPJct9GFHJWx4a3H9l1b1vnvWuA56Bm/nTvQbAMAQxem373J+o0h+lvYPi66XUAu+AwWr8TWsEi8wxYYqsZ+aa0FcuTVoCEd8MS6MRy9tzXiZdb5xBBm/VNq3xXrqufDLmxXKh1CrvViMZB9gwN8fjdSUXDIm4gOVx0LxJFe+uddN0320uIqNhR3X5H5xwe/SE4L3mwSJcoSrDav9UiiyKOwfNhg==", + "paypal-transmission-time": "2023-03-03T23:51:25Z", + "x-b3-spanid": "c17d3c8668470309" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.SUSPENDED", + "name": "BILLING.SUBSCRIPTION.SUSPENDED", + "body": { + "id": "WH-0RD44774E41721427-487281337V896051W", + "event_version": "1.0", + "create_time": "2016-04-28T11:37:14Z", + "resource_type": "Agreement", + "event_type": "BILLING.SUBSCRIPTION.SUSPENDED", + "summary": "A billing subscription was suspended", + "resource": { + "id": "I-PE7JWXKGVN0R", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + }, + "plan": { + "curr_code": "USD", + "links": [], + "payment_definitions": [ + { + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "5.00" + }, + "cycles": "5", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "1.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + }, + { + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "10.00" + }, + "cycles": "15", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "2.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + } + ], + "merchant_preferences": { + "setup_fee": { + "value": "0.00" + }, + "auto_bill_amount": "YES", + "max_fail_attempts": "21" + } + }, + "payer": { + "payment_method": "paypal", + "status": "verified", + "payer_info": { + "email": "coolbuyer@example.com", + "first_name": "Cool", + "last_name": "Buyer", + "payer_id": "XLHKRXRA4H7QY", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + } + } + }, + "agreement_details": { + "outstanding_balance": { + "value": "0.00" + }, + "num_cycles_remaining": "5", + "num_cycles_completed": "0", + "last_payment_date": "2016-04-28T11:29:54Z", + "last_payment_amount": { + "value": "1.00" + }, + "final_payment_due_date": "1971-07-30T10:00:00Z", + "failed_payment_count": "0" + }, + "description": "desc", + "state": "Suspended", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-agreements/I-PE7JWXKGVN0R", + "rel": "self", + "method": "GET" + } + ], + "start_date": "2016-04-30T07:00:00Z" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-0RD44774E41721427-487281337V896051W", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-0RD44774E41721427-487281337V896051W/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e3f572*Host=ccg14amqunphttpdeliveryd46", + "client_pid": "761921", + "correlation-id": "f6e5d463df567", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "58edad30-ba1e-11ed-a42f-c5267af5ab60", + "paypal-transmission-sig": "eS9blYRTmTAptEoqlQ+Tv/9d4/EpxRGiyOX9v4t0s9gHOmRzHLr1fLyBsZD+IZixI5QAMe3JMszLOcjPrfAlBYkXWvToREUsuwlLVeWJkB4zlFyanQxX+z/5h//6tKKlxPaKUJglkHU3JLY6YcanvZ3HRxe237Ngtum+6xYO41q2EYxSuP+cKSCLBcY0LaDRO5M6Zwz+lCKLax0lSBZym7YrZqSVbAux2wqvhWgeXlFwnR4NS2+NHewZ23z4H3w/0+EyYIX/UgtUu8+HdvkkaakUUD21gW4QneAEx0Gs3AVXK5FxTuarCyX01IesrVGSnBdTCZHIg1lhV4XHSPpFAw==", + "paypal-transmission-time": "2023-03-03T23:51:42Z", + "x-b3-spanid": "50ff6b21e56a0184" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "BILLING.SUBSCRIPTION.UPDATED", + "name": "BILLING.SUBSCRIPTION.UPDATED", + "body": { + "id": "WH-7BW55401AV391063H-02P24463AR970092S", + "event_version": "1.0", + "create_time": "2016-04-28T11:43:08Z", + "resource_type": "Agreement", + "event_type": "BILLING.SUBSCRIPTION.UPDATED", + "summary": "A billing subscription was updated", + "resource": { + "id": "I-PE7JWXKGVN0R", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + }, + "plan": { + "curr_code": "USD", + "links": [], + "payment_definitions": [ + { + "type": "TRIAL", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "5.00" + }, + "cycles": "5", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "1.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + }, + { + "type": "REGULAR", + "frequency": "Month", + "frequency_interval": "1", + "amount": { + "value": "10.00" + }, + "cycles": "15", + "charge_models": [ + { + "type": "TAX", + "amount": { + "value": "2.00" + } + }, + { + "type": "SHIPPING", + "amount": { + "value": "1.00" + } + } + ] + } + ], + "merchant_preferences": { + "setup_fee": { + "value": "0.00" + }, + "auto_bill_amount": "YES", + "max_fail_attempts": "21" + } + }, + "payer": { + "payment_method": "paypal", + "status": "verified", + "payer_info": { + "email": "coolbuyer@example.com", + "first_name": "Cool", + "last_name": "Buyer", + "payer_id": "XLHKRXRA4H7QY", + "shipping_address": { + "recipient_name": "Cool Buyer", + "line1": "3rd st", + "line2": "cool", + "city": "San Jose", + "state": "CA", + "postal_code": "95112", + "country_code": "US" + } + } + }, + "agreement_details": { + "outstanding_balance": { + "value": "0.00" + }, + "num_cycles_remaining": "5", + "num_cycles_completed": "0", + "last_payment_date": "2016-04-28T11:29:54Z", + "last_payment_amount": { + "value": "1.00" + }, + "final_payment_due_date": "1971-07-30T10:00:00Z", + "failed_payment_count": "0" + }, + "description": "update desc", + "state": "Suspended", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/billing-agreements/I-PE7JWXKGVN0R", + "rel": "self", + "method": "GET" + } + ], + "start_date": "2016-04-30T07:00:00Z" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-7BW55401AV391063H-02P24463AR970092S", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-7BW55401AV391063H-02P24463AR970092S/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e4356e*Host=ccg13amqunphttpdeliveryd22", + "client_pid": "766546", + "correlation-id": "b51e908e399be", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "5ba5c850-ba1e-11ed-b26e-4700f011de08", + "paypal-transmission-sig": "cLP3Nk1bmFwbnJhNQ1I2+j2U5sxGftSmmB0lrIrR3TSiS9sfbVyvxdBIRj3V9AmJKUgcvr8lMwEhXQN42WTlLUJWKdZIh9QXmihYMHUna8AMzxV14OcBm3Y9O+Ngu4FWudr3EE/VPtQLkoxIgHRGljpLvOgz71ufRGLZGUzRZOtNVx000CPAHwtwLyfCS4JYR6MF26tWoJRagwQ7jflU9aQlgA5DYvxAlHsSZ5gI96CadIU/0tW+huYnJM7loJLwUOIiPHWBEn+eMEIkEiLRuo7C+N3etuiMf3oDHaobirnEhTjmmMyAKrucBwv06l+pzx+Yv1GZeGdDRwfDtCm45w==", + "paypal-transmission-time": "2023-03-03T23:51:46Z", + "x-b3-spanid": "872d5fc27a42d9ca" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CATALOG.PRODUCT.CREATED", + "name": "CATALOG.PRODUCT.CREATED", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R661687G", + "create_time": "2018-12-19T22:20:32.000Z", + "event_type": "CATALOG.PRODUCT.CREATED", + "event_version": "1.0", + "resource_type": "product", + "resource_version": "2.0", + "summary": "Product created", + "resource": { + "id": "72255d4849af8ed6e0df1173", + "name": "Video Streaming Service", + "description": "Video streaming service", + "type": "SERVICE", + "category": "SAAS", + "image_url": "https://example.com/streaming.jpg", + "home_url": "https://example.com/home", + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/catalogs/products/72255d4849af8ed6e0df1173", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/catalogs/products/72255d4849af8ed6e0df1173", + "rel": "edit", + "method": "PATCH" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e3dfa6*Host=ccg14amqunphttpdeliveryd39", + "client_pid": "761914", + "correlation-id": "d3a3ebfe3dbf4", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "5ea1f150-ba1e-11ed-a5f7-a56ac798bf6f", + "paypal-transmission-sig": "bO3qgys2KPjDTXm/4ti5GzhkGC/+YnxppjPdYUVHCq7P5JNfsvXw+6d9VEZncj6453vwuCBwDaHaEcm2hV3b9DWCiBCzqwByfecxd+iV8nNKUdJYzWXuTXmsGlzmtuVzZd6dZsS+IgjUlKTOpS++WqmSLnyWJMzJ9S2DZvqn9YCrknbI79bTmq0A/l4zxC10VoxRo1iwdWId+zxAVNVc0NlZHJhpMNs7lWgi9YmAsK0DEfFLHcBWDVPFvKXZkViDrg/Gk/ro9U7AWSTO4kavMyQ8mZX+tSAc1AaSLuJMj+MzYayIo0z+RqHZKVCTpp9bWhXTjCaQd2K5DdXjpj7DEQ==", + "paypal-transmission-time": "2023-03-03T23:51:51Z", + "x-b3-spanid": "c079798047992471" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CATALOG.PRODUCT.UPDATED", + "name": "CATALOG.PRODUCT.UPDATED", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R661687G", + "create_time": "2018-12-19T22:20:32.000Z", + "event_type": "CATALOG.PRODUCT.UPDATED", + "event_version": "1.0", + "resource_type": "product", + "resource_version": "2.0", + "summary": "Product updated", + "resource": { + "id": "PROD-VFGB2314GFDE7658N", + "name": "Video Streaming Service", + "description": "Video streaming service", + "type": "SERVICE", + "category": "SAAS", + "image_url": "https://example.com/streaming.jpg", + "home_url": "https://example.com/home", + "create_time": "2018-12-10T21:20:49Z", + "update_time": "2018-12-10T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v1/catalogs/products/72255d4849af8ed6e0df1173", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/catalogs/products/72255d4849af8ed6e0df1173", + "rel": "edit", + "method": "PATCH" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e3f752*Host=ccg14amqunphttpdeliveryd37", + "client_pid": "764189", + "correlation-id": "b229fe9a4534b", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "60fa1220-ba1e-11ed-b284-5163cdac11a1", + "paypal-transmission-sig": "HTiHrgr2Qp4XIsMHKKnVMROFgX9ZDer5oVhnrG6guDzGAabEO6UZvTOkAqltyZRtGAVhuYAxQ/wxNsLadnVE4rfPGhZp8OfY9Qk6by4WNhMkaKgb7OIBj3ataVZ8bMU/uOc+ve9tg2rmusZWDjzGJGNWrrAgpP6cxVkQr5V0d44At/LLI2G3CWZkZAiw/55e2FDPxuAnq9U66+jl1Ioe1J9GwzqmTuVxs7mPvKObwVNf1lOkB8Ko/4ijWbsM0QvAvM2rzJeCUsaGb3sbP9jwAXxQBb7oQb6LQ+XIDAubCvkuMV1jqQ0qts9gM257zbOzDmqvy5SC/qcCI9EaoJmPdA==", + "paypal-transmission-time": "2023-03-03T23:51:55Z", + "x-b3-spanid": "8e5ea768f6be199e" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CHECKOUT.CHECKOUT.BUYER-APPROVED", + "name": "CHECKOUT.CHECKOUT.BUYER-APPROVED", + "body": { + "id": "WH-6W4482673W002281V-61985753LP2332451", + "create_time": "2018-19-12T22:20:32.000Z", + "event_type": "CHECKOUT.CHECKOUT.BUYER-APPROVED", + "event_version": "1.0", + "resource_type": "payment", + "summary": "Checkout Payment is created and approved by buyer.", + "resource": { + "id": "EC-7JW39463WG7174715", + "payload": "payment_type=instant&payment_date=10%3A46%3A54%20Apr%2028%2C%202019%20PDT&payment_status=Completed&address_status=confirmed&payer_status=verified&first_name=John&last_name=Smith&payer_email=buyer@paypalsandbox.com&payer_id=TESTBUYERID01&address_name=John%20Smith&address_country=United%20States&address_country_code=US&address_zip=95131&address_state=CA&address_city=San%20Jose&address_street=123%20any%20street&business=seller@paypalsandbox.com&receiver_email=seller@paypalsandbox.com&receiver_id=seller@paypalsandbox.com&residence_country=US&item_name1=something&item_number1=AK-1234&tax=2.02&mc_currency=USD&mc_fee=0.44&mc_gross=12.34&mc_gross_1=12.34&mc_handling=2.06&mc_handling1=1.67&mc_shipping=3.02&mc_shipping1=1.02&txn_type=cart&txn_id=274113719¬ify_version=2.1&custom=xyz123&invoice=abc1234&test_ipn=1&verify_sign=AmoLQMSQnFNTX4UH11dp7XvPK.4bAfd10dnRIZeo9cONJNn0b9NquIGb" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-6W4482673W002281V-61985753LP2332451", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-6W4482673W002281V-61985753LP2332451/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e203fc*Host=ccg13amqunphttpdeliveryd4", + "client_pid": "770183", + "correlation-id": "7aa54eb41e894", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "0d3f9790-ba1e-11ed-9150-2f866594276d", + "paypal-transmission-sig": "QSAXlIIUdEu8atHfhbnwpnxKI444ZX+xNb2jNi/Yj05bV5uR3EpWhq1y89SVxKLPXSdZocC0ibstVA9Dycd4XJTHo1BkYLkMfwBCDf7F3W6aQnMjkGST+FiUNT6etYtIsWQhRcH+k/J5krNj5pmabfOZQ/b1oPQHsDA62ctokZchUCkjjJEMVX7b5rHvJIFJa+2BQnBj2EUHdDVsz3MoU43uxqMjK1sIYsVDltOG1Dx+fgbjv4aGkHv6xkapnUGrad31eclbmfG/J2PQ5rqnY/lhq6g+fnD9UtDxq9gC8PXBHa2tIMPAUNMnK1Z/orCshYUTteYZc82B28staFPn/Q==", + "paypal-transmission-time": "2023-03-03T23:49:35Z", + "x-b3-spanid": "16fa6c3ed35f4113" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CHECKOUT.ORDER.APPROVED", + "name": "CHECKOUT.ORDER.APPROVED", + "body": { + "id": "WH-COC11055RA711503B-4YM959094A144403T", + "create_time": "2018-04-16T21:21:49.000Z", + "event_type": "CHECKOUT.ORDER.APPROVED", + "resource_type": "checkout-order", + "resource_version": "2.0", + "summary": "An order has been approved by buyer", + "resource": { + "id": "5O190127TN364715T", + "status": "APPROVED", + "intent": "CAPTURE", + "payer": { + "name": { + "given_name": "John", + "surname": "Doe" + }, + "email_address": "customer@example.com", + "payer_id": "QYR5Z8XDVJNXQ" + }, + "purchase_units": [ + { + "reference_id": "d9f80740-38f0-11e8-b467-0ed5f89f718b", + "amount": { + "currency_code": "USD", + "value": "100.00" + }, + "payee": { + "email_address": "merchant@example.com" + }, + "shipping": { + "method": "United States Postal Service", + "address": { + "address_line_1": "2211 N First Street", + "address_line_2": "Building 17", + "admin_area_2": "San Jose", + "admin_area_1": "CA", + "postal_code": "95131", + "country_code": "US" + } + } + } + ], + "create_time": "2018-04-01T21:18:49Z", + "update_time": "2018-04-01T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T/capture", + "method": "POST" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-COC11055RA711503B-4YM959094A144403T", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-COC11055RA711503B-4YM959094A144403T/resend", + "rel": "resend", + "method": "POST" + } + ], + "event_version": "1.0" + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e46390*Host=ccg14amqunphttpdeliveryd50", + "client_pid": "762622", + "correlation-id": "6d86a402e064e", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "65f03660-ba1e-11ed-b284-5163cdac11a1", + "paypal-transmission-sig": "dn4KcrSaxCEXd8I5yrHQWJtQ4FySM1Bfsob4dBTBo7FSGrNJYQF2YjiX1pkKI+/MvPHWPIA62aWIT5dqxl1RLxvuYmZOf1O6SRx5XVcil9zotqdAADDsC/7V91qb5Qh6mMUv/Hut11ny0ZEAJoUsc88O/XVchuwuDYbYU+WhCmdUywnposyyoddELfd/TSWv3BsW9a+K0/iUU3T2Yg3o7mxSP0jPlv/WiwAOqv7EABhkhdLlZIOznJVOwSeDp8W9Z3XiNpy1kWDR26YWqeM0RSjCq1k9CjHMjIGKm2SN8KymTKzE44eblh/Onm5LXXeiuOyQ636fnZOp5nyU91+O0Q==", + "paypal-transmission-time": "2023-03-03T23:52:04Z", + "x-b3-spanid": "9f4a678094f242d4" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CHECKOUT.ORDER.COMPLETED", + "name": "CHECKOUT.ORDER.COMPLETED", + "body": { + "id": "WH-COC11055RA711503B-4YM959094A144403T", + "create_time": "2018-04-16T21:21:49.000Z", + "event_type": "CHECKOUT.ORDER.COMPLETED", + "resource_type": "checkout-order", + "resource_version": "2.0", + "summary": "Checkout Order Completed", + "resource": { + "id": "5O190127TN364715T", + "status": "COMPLETED", + "intent": "CAPTURE", + "gross_amount": { + "currency_code": "USD", + "value": "100.00" + }, + "payer": { + "name": { + "given_name": "John", + "surname": "Doe" + }, + "email_address": "buyer@example.com", + "payer_id": "QYR5Z8XDVJNXQ" + }, + "purchase_units": [ + { + "reference_id": "d9f80740-38f0-11e8-b467-0ed5f89f718b", + "amount": { + "currency_code": "USD", + "value": "100.00" + }, + "payee": { + "email_address": "seller@example.com" + }, + "shipping": { + "method": "United States Postal Service", + "address": { + "address_line_1": "2211 N First Street", + "address_line_2": "Building 17", + "admin_area_2": "San Jose", + "admin_area_1": "CA", + "postal_code": "95131", + "country_code": "US" + } + }, + "payments": { + "captures": [ + { + "id": "3C679366HH908993F", + "status": "COMPLETED", + "amount": { + "currency_code": "USD", + "value": "100.00" + }, + "seller_protection": { + "status": "ELIGIBLE", + "dispute_categories": [ + "ITEM_NOT_RECEIVED", + "UNAUTHORIZED_TRANSACTION" + ] + }, + "final_capture": true, + "seller_receivable_breakdown": { + "gross_amount": { + "currency_code": "USD", + "value": "100.00" + }, + "paypal_fee": { + "currency_code": "USD", + "value": "3.00" + }, + "net_amount": { + "currency_code": "USD", + "value": "97.00" + } + }, + "create_time": "2018-04-01T21:20:49Z", + "update_time": "2018-04-01T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v2/payments/captures/3C679366HH908993F", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v2/payments/captures/3C679366HH908993F/refund", + "rel": "refund", + "method": "POST" + } + ] + } + ] + } + } + ], + "create_time": "2018-04-01T21:18:49Z", + "update_time": "2018-04-01T21:20:49Z", + "links": [ + { + "href": "https://api.paypal.com/v2/checkout/orders/5O190127TN364715T", + "rel": "self", + "method": "GET" + } + ] + }, + "links": [ + { + "href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-COC11055RA711503B-4YM959094A144403T", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-COC11055RA711503B-4YM959094A144403T/resend", + "rel": "resend", + "method": "POST" + } + ], + "zts": 1494957670, + "event_version": "1.0" + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e493d8*Host=ccg14amqunphttpdeliveryd47", + "client_pid": "760827", + "correlation-id": "d7b5e9cb06c81", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "689abcf0-ba1e-11ed-8635-fb7d730e54d3", + "paypal-transmission-sig": "IymBh75Dezm92b8XACF4mrXPWN8STab98czZHmd9BKrx0ty6SgJV/tkMmRORgebw/sXnvbgXSQDjv/kXrOsLWAR/A4jeU6ClIVKdfxaXeD4AJV6ufvepeQtfd91iClE1f2NSAKDrMCGmFtsfm/6XS9oTsHa/EGPH5ircl++7as3EfcTIMibDcieTpXIrdBJygrybMw4jNnV3inWD3UQ8ejhUCcKNICnMozAuQL0geWCURdvShrZZM0cKoukgK0oWNHecyVKjT81Dzh/2VCSFqazpOV3mCGGUo72le8LrisorDAha5KUsIf5iUrmpLHeFdWAfAlgqFrYU5xPvFD8obg==", + "paypal-transmission-time": "2023-03-03T23:52:08Z", + "x-b3-spanid": "51240d0a74d272b9" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CHECKOUT.ORDER.SAVED", + "name": "CHECKOUT.ORDER.SAVED", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R661687G", + "create_time": "2018-05-03T22:20:32.000Z", + "event_type": "CHECKOUT.ORDER.SAVED", + "event_version": "1.0", + "resource_type": "order", + "resource_version": "2.0", + "summary": "Order 9NK63094MA726991F for 100 USD has been saved.", + "resource": { + "id": "9NK63094MA726991F", + "intent": "AUTHORIZE", + "purchase_units": [ + { + "reference_id": "REDFWE35G", + "amount": { + "currency_code": "USD", + "value": "100.00" + }, + "payee": { + "email_address": "pbehara-seller@paypal.com", + "merchant_id": "WDMEWZ63SR74G" + } + } + ], + "create_time": "2019-03-06T21:42:35Z", + "links": [ + { + "href": "https://api.paypal.com/v2/checkout/orders/9NK63094MA726991F", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/checkoutnow?token=9NK63094MA726991F", + "rel": "approve", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v2/checkout/orders/9NK63094MA726991F", + "rel": "update", + "method": "PATCH" + }, + { + "href": "https://api.paypal.com/v2/checkout/orders/9NK63094MA726991F/save", + "rel": "save", + "method": "POST" + } + ], + "status": "SAVED" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e4ad8c*Host=ccg14amqunphttpdeliveryd66", + "client_pid": "763871", + "correlation-id": "26a1589f267c6", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "6b1fe130-ba1e-11ed-9ef6-d38729134cb6", + "paypal-transmission-sig": "gzPWDX/yJ5H1M/OIMiuHH6XzXMT05jy5JFopFr1hmDUxD7+pbevI6gcZSHOdW2FXAFZTF+iRbgzE0ny2lTLny0I/kGaiJ82nRsNNxz1XtchCC+LphRFpMTqSfO1CZWMLS4a1VWe8A//W899nWCmK8uBvuVwmpjbOhn2D1B0i627mKPT6fLiyT9mriUXD7/dOrRRVrV0CZh5tFdJyOWZ8wDIm7gqSfU60EJgLiR+mAgVtw1WtaX+IEBX9xVkTv/SpYkdattH9TBppRL5rNlEnghinLD2/xlO7Ve9g3RfsevSRCAizyAo3e9izbs7XwswsB8fakCK0B37cFkM4PzEKWQ==", + "paypal-transmission-time": "2023-03-03T23:52:12Z", + "x-b3-spanid": "8f603af310e885a6" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CHECKOUT.ORDER.VOIDED", + "name": "CHECKOUT.ORDER.VOIDED", + "body": { + "id": "WH-55TG7562XN2588878-8YH955435R661687G", + "create_time": "2018-05-03T22:20:32.000Z", + "event_type": "CHECKOUT.ORDER.VOIDED", + "event_version": "1.0", + "resource_type": "order", + "resource_version": "2.0", + "summary": "An order 9NK63094MA726991F for 100 USD was voided.", + "resource": { + "id": "9NK63094MA726991F", + "intent": "AUTHORIZE", + "purchase_units": [ + { + "reference_id": "REDFWE35G", + "amount": { + "currency_code": "USD", + "value": "100.00" + }, + "payee": { + "email_address": "pbehara-seller@paypal.com", + "merchant_id": "WDMEWZ63SR74G" + } + } + ], + "create_time": "2019-03-06T21:42:35Z", + "status": "VOIDED", + "links": [ + { + "href": "https://api.paypal.com/v2/checkout/orders/9NK63094MA726991F", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/checkoutnow?token=9NK63094MA726991F", + "rel": "approve", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v2/checkout/orders/9NK63094MA726991F", + "rel": "update", + "method": "PATCH" + }, + { + "href": "https://api.paypal.com/v2/checkout/orders/9NK63094MA726991F/save", + "rel": "save", + "method": "POST" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-55TG7562XN2588878-8YH955435R661687G/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e49aa4*Host=ccg13amqunphttpdeliveryd42", + "client_pid": "772760", + "correlation-id": "1e499050764f4", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "6dcd26e0-ba1e-11ed-b9b7-69139b55be60", + "paypal-transmission-sig": "YAt1roNA8zKecx+5+DLtjiNk+Vjwwbu+IQqLwBfBdboSKKFXoipQvj2EszkmIDX5Fee8HGT3ufzrDkQoJbFiZY5enaLgxB8Lm9zv673BIY86slYOAk1ixhfrNpz3bqU/31cCQVJttG5O0z76R9GezYMCbm+Q9iQVkR4Gw92MabPzXHX31NaJn9DssJHV+DDqqJKmmVAHJnPw05merbjDs+eQ0tA2jQgRsaJt7KdBNTAbSTjLesHhh81qQPwsJYXDScm9G0TMW9kWHfA0HNFYICeLeBwrv3HZHjpXCZMGeRVSWLc3aNzvgv7DkzISxp1pHyt5h935S89cH6Q3304pKQ==", + "paypal-transmission-time": "2023-03-03T23:52:17Z", + "x-b3-spanid": "e3e54ed340b26941" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CUSTOMER-SUPPORT.CHARGEBACK.DECISION-RESPONDED", + "name": "CUSTOMER-SUPPORT.CHARGEBACK.DECISION-RESPONDED", + "body": { + "id": "WH-14672365205054426-9R575015BN3466209", + "event_version": "1.0", + "create_time": "2021-07-05T09:31:15.000Z", + "resource_type": "chargeback", + "event_type": "CUSTOMER-SUPPORT.CHARGEBACK.DECISION-RESPONDED", + "summary": "Chargeback decision response sent to tenant successfully.", + "resource": { + "transaction_id": null, + "id": "BTCBP96", + "stage": "CHARGEBACK", + "amount": { + "currency_code": "USD", + "value": "150" + }, + "originated_time": "2021-06-21T03:00:05Z", + "received_time": "2021-06-23T05:55:05Z", + "processor_reference_number": "WORLDLINE", + "response_by_time": "2021-07-09T13:30:07Z", + "contestation": { + "evidences": [ + { + "document_id": "001", + "document_name": "contestation document", + "document_source": "TENANT" + }, + { + "document_id": "002", + "document_name": "compelling evidence proof of delivery", + "document_source": "SELLER" + } + ], + "compelling_evidences": [ + { + "evidence_type": "PROOF_OF_DELIVERY" + }, + { + "evidence_type": "PROOF_OF_SAME_MERCHANDISE_NONDISPUTED", + "supporting_details": [ + { + "name": "ARN", + "value": "123abc" + }, + { + "name": "TRANSACTION_DATE", + "value": "2021-06-14" + }, + { + "name": "PHYSICAL_ADDRESS", + "value": "Seattle, WA" + } + ] + } + ], + "rebuttal_evidences": [ + { + "evidence_type": "SIGNED_ORDER_FORM" + } + ] + }, + "decision_notes": "We are contesting this chargeback", + "external_transaction_id": "567GssErT50", + "case_id": "345BhiJoLL", + "external_case_id": "NzM4MTAx123", + "decision": "CONTEST" + }, + "links": [ + { + "href": "https://msmaster.g.devqa.gcp.dev.paypalinc.com:14084/v1/notifications/webhooks-events/WH-14672365205054426-9R575015BN3466209", + "rel": "self", + "method": "GET" + }, + { + "href": "https://msmaster.g.devqa.gcp.dev.paypalinc.com:14084/v1/notifications/webhooks-events/WH-14672365205054426-9R575015BN3466209/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e43e6a*Host=ccg14amqunphttpdeliveryd40", + "client_pid": "764846", + "correlation-id": "8f57e30262ee7", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "72b0d490-ba1e-11ed-aae9-bfc5d90cc1da", + "paypal-transmission-sig": "Dy3H4ZMaVOtjQqCoKuqurt1XVRvlTLx9rf5/AP3VHb8Yxc0ZbGH6bcKZF5KmhRh7qcb+DIfNKfM7Kr4nYETHcYltQ7z0sXOueEZcvqezXiNsTIbEZHvIAv+cQEA6VuVNk7V4vbsgDO5elltdgnJIvP32Pu4PhzcIikH+fohV5LiG24yvLA6bkVs91LpODoqmyq+XY28l2+4rqokLxLQR72Wp0MMBT6aRTKS7rgNHGtzmhx7Ih7d0gw5sCZeDuUvKZHBvnpu9KJ7xjfucjX7jNzexAGwHIqn4BeJIbAOe4GiUnbVTOCHbJGjrxjNN+WuDfjVgGxph38NDnxvNC07YBg==", + "paypal-transmission-time": "2023-03-03T23:52:25Z", + "x-b3-spanid": "c83b4523cde72ef" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CUSTOMER.DISPUTE.CREATED", + "name": "CUSTOMER.DISPUTE.CREATED", + "body": { + "id": "WH-4M0448861G563140B-9EX36365822141321", + "event_version": "1.0", + "create_time": "2018-06-21T13:36:33.000Z", + "resource_type": "dispute", + "event_type": "CUSTOMER.DISPUTE.CREATED", + "summary": "A new dispute opened with Case # PP-000-042-663-135", + "resource": { + "dispute_id": "PP-000-042-663-135", + "create_time": "2018-06-21T13:35:44.000Z", + "update_time": "2018-06-21T13:35:44.000Z", + "disputed_transactions": [ + { + "seller_transaction_id": "00D10444LD479031K", + "seller": { + "merchant_id": "RD465XN5VS364", + "name": "Test Store" + }, + "items": [], + "seller_protection_eligible": true + } + ], + "reason": "MERCHANDISE_OR_SERVICE_NOT_RECEIVED", + "status": "OPEN", + "dispute_amount": { + "currency_code": "USD", + "value": "3.00" + }, + "dispute_life_cycle_stage": "INQUIRY", + "dispute_channel": "INTERNAL", + "messages": [ + { + "posted_by": "BUYER", + "time_posted": "2018-06-21T13:35:52.000Z", + "content": "qwqwqwq" + } + ], + "links": [ + { + "href": "https://api.paypal.com/v1/customer/disputes/PP-000-042-663-135", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/customer/disputes/PP-000-042-663-135/send-message", + "rel": "send_message", + "method": "POST" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-4M0448861G563140B-9EX36365822141321", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-4M0448861G563140B-9EX36365822141321/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e2721a*Host=ccg14amqunphttpdeliveryd47", + "client_pid": "760267", + "correlation-id": "f0a9bcd1c3a20", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "12caf6f0-ba1e-11ed-8e1c-256216f79c11", + "paypal-transmission-sig": "krwAsHOnNUtYsqIAJQEQTgjSIeVdByhyQje27fXo4tKwaTCT7RLUGUaK9S2F595nM3LRd3jPjyDvtDSlEKGQX/eWkduyzMi8OpJjDRCwm/QkKMFNkUHtwmnjK2imd7cztpJgjFnU/g4sUz4WUessE7DgAoUKtjT2VG5usidY5SfYdTQ4ag/N4us8qFgp4jpubuCuBgi3NCyoKaxh5FVNCEyjV3HKBTKj1DPAr3bwdkMEuUIGcsisq+qa1M5Fqixk09W6SulKTDpNP4+3+TB9JsBAI5kR/CKvFoiP45RsGr1VmQLXTN3TOCyJpsyFC55nKf4X00Up+U+Ona9a6qehKQ==", + "paypal-transmission-time": "2023-03-03T23:49:44Z", + "x-b3-spanid": "24c1e9041a5ea4dc" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CUSTOMER.MANAGED-ACCOUNT.UPDATED", + "name": "CUSTOMER.MANAGED-ACCOUNT.UPDATED", + "body": { + "id": "WH-3AE50806EM5305732-8C457463XB353952R", + "event_version": "1.0", + "create_time": "2018-05-24T07:18:28.264Z", + "resource_type": "managed-accounts", + "event_type": "CUSTOMER.MANAGED-ACCOUNT.UPDATED", + "summary": "A merchant account was updated.", + "resource": { + "payer_id": "7G4EPEEPEF74L", + "external_id": "demo-201@shopify.com", + "country_code": "US", + "email_id": "UNIQUE@GENERATED.com", + "name": { + "prefix": "Mr.", + "given_name": "John", + "surname": "Collins", + "middle_name": "Davis", + "suffix": "Jr" + }, + "addresses": [ + { + "type": "HOME", + "address_line_1": "150 E San Fernando St", + "address_line_2": "apt #1", + "admin_area_1": "CA", + "admin_area_2": "San Jose", + "postal_code": "95112", + "country_code": "US" + } + ], + "phones": [ + { + "type": "MOBILE", + "country_code": "1", + "national_number": "4089679174" + } + ], + "identifications": [ + { + "type": "DRIVERS_LICENSE", + "identifier": "G1111111", + "issuer": { + "country_code": "US", + "jurisdiction": "CA", + "authority": "DMV CA" + } + } + ], + "tax_id": { + "type": "SOCIAL_SECURITY_NUMBER", + "identifier": "123456789", + "issuer": { + "country_code": "US", + "authority": "IRS" + } + }, + "citizenship": "US", + "date_of_birth": "1990-01-01", + "business_info": { + "type": "INDIVIDUAL", + "names": [ + { + "type": "LEGAL", + "business_name": "Demo 201 Inc." + } + ], + "addresses": [ + { + "type": "WORK", + "address_line_1": "585 Franklin Str", + "address_line_2": "apt #1", + "admin_area_1": "CA", + "admin_area_2": "Mountain View", + "postal_code": "94041", + "country_code": "US" + } + ], + "email_address": "customer-service@demo201.com", + "phones": [ + { + "type": "CUSTOMER_SERVICE", + "country_code": "1", + "national_number": "4089673333" + } + ], + "identifications": [ + { + "type": "TAX_IDENTIFICATION_NUMBER", + "identifier": "111234501", + "issuer": { + "country_code": "US", + "authority": "IRS" + } + } + ], + "date_of_incorporation": "2001-01-17", + "business_category": { + "merchant_category_code": "3011" + }, + "website": "https://www.demo201.com", + "average_monthly_volume": { + "minimum_amount": { + "currency_code": "USD", + "value": "1000.00" + }, + "maximum_amount": { + "currency_code": "USD", + "value": "2000.00" + } + }, + "stakeholders": [ + { + "id": "ext_stakeholer_01", + "ownership_percentage": "100.0", + "type": "BENEFICIAL_OWNER", + "name": { + "prefix": "Mr.", + "given_name": "John", + "surname": "Collins", + "middle_name": "Davis", + "suffix": "Jr" + }, + "citizenship": "US", + "date_of_birth": "1990-01-01", + "addresses": [ + { + "address_line_1": "150 E San Fernando St", + "address_line_2": "apt #1", + "admin_area_1": "CA", + "admin_area_2": "San Jose", + "postal_code": "95112", + "country_code": "US" + } + ], + "phones": [ + { + "country_code": "1", + "national_number": "4089679174" + } + ], + "identifications": [ + { + "type": "DRIVERS_LICENSE", + "identifier": "G1111111", + "issuer": { + "country_code": "US", + "jurisdiction": "CA", + "authority": "DMV CA" + } + } + ], + "tax_id": { + "type": "SOCIAL_SECURITY_NUMBER", + "identifier": "123456789", + "issuer": { + "country_code": "US", + "authority": "IRS" + } + } + } + ] + }, + "primary_currency_code": "USD", + "secondary_currency_codes": [ + "CAD" + ], + "financial_instruments": { + "banks": [ + { + "mandate": { + "acceptance_time": "2018-05-12T01:23:45.678Z", + "accepted": true + }, + "id": "BA-KTJ7Z4FBMELZA", + "last_4_digits": "0937", + "account_number_type": "BASIC_BANK_ACCOUNT_NUMBER", + "account_type": "CHECKING", + "routing_number": "081500875", + "bank_name": "Wells Fargo Bank", + "bic": "WFBIUS6WFFX", + "account_holder_details": { + "name": "John Davis Collins", + "type": "BUSINESS" + } + } + ] + }, + "cc_soft_descriptor": "DEMO-201", + "terms_and_conditions_acceptance": { + "acceptance_time": "2018-05-12T01:23:45.678Z", + "accepted": true + }, + "organization": "us/shops", + "user_id": "demo-201FB", + "capabilities": [ + { + "name": "WITHDRAW_MONEY", + "status": "SUSPENDED", + "conditions": { + "threshold": "AMOUNT", + "threshold_value": "5000.00 USD", + "remaining_value": "1000.00 USD" + }, + "required_fields": [ + { + "name": "date_of_birth", + "link": { + "rel": "edit", + "href": "https://api.paypal.com/v2/customer/managed-accounts/7G4EPEEPEF74L", + "method": "PATCH" + } + } + ], + "required_documents": [ + { + "name": "PASSPORT", + "link": { + "rel": "upload", + "href": "https://api.paypal.com/v1/customer/supporting-documents/7G4EPEEPEF74LFG/upload", + "method": "POST" + } + } + ] + } + ], + "regulatory_obligations": [ + { + "name": "MARKETPLACES_KYC", + "status": "COMPLETED" + } + ], + "links": [ + { + "rel": "self", + "href": "https://api.paypal.com/v2/customer/managed-accounts/7G4EPEEPEF74L", + "method": "PATCH" + }, + { + "rel": "edit", + "href": "https://api.paypal.com/v2/customer/managed-accounts/7G4EPEEPEF74L", + "method": "PATCH" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notfications/webhooks-events/WH-3AE50806EM5305732-8C457463XB353952R", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notfications/webhooks-events/WH-3AE50806EM5305732-8C457463XB353952R/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e237d2*Host=ccg14amqunphttpdeliveryd18", + "client_pid": "761923", + "correlation-id": "5e82bedfcbe34", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "16fe8e80-ba1e-11ed-a43c-7bbaa77c6dea", + "paypal-transmission-sig": "Htt7OSJ78RBzMAMHoAZdCsftVfAHeYUcGcFFG4Ek5l3ffoant99155FUn9kiLNopDxqcwZoDj4aVN/4XhI0dsGj5oETlOPqUXhC0IPL8Nn+2BP62YvFuYwYnDI6A4oG/q/WbgHg1TtmFe4+lPlXzQ1DZAQYKkTTFbPwQRbuGPt5eFGGolE6M8T9i8EOoMkJ6WA97/Zy/5Jxy0z1xQpoxAOEH2yRZfYtmsSyJPzqMicRXFkl9L6T84UoYkYxGsHpLjueDBwd6Pq0/k12vElj3E+Km8KRRJj+4+6Ckv43/iFYAIRxF/24hbU65sijyPLy66E0suSA+ia/QGaR4Mxg2Pg==", + "paypal-transmission-time": "2023-03-03T23:49:51Z", + "x-b3-spanid": "d5451fdbc4219a51" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "CUSTOMER.PAYOUT.COMPLETED", + "name": "CUSTOMER.PAYOUT.COMPLETED", + "body": { + "id": "WH-29H04174VA171590Y-15T39252MK897181P", + "event_version": "1.0", + "create_time": "2018-06-29T18:39:07.118Z", + "resource_type": "payouts", + "event_type": "CUSTOMER.PAYOUT.COMPLETED", + "summary": "Payout has completed.", + "resource": { + "id": "4324324DSFDSFDSF", + "merchant_id": "ZLFUUPW93YVFY", + "destination": { + "id": "CC-CBYJLKC9HGN7S", + "type": "FINANCIAL_INSTRUMENT_ID" + }, + "amount": { + "currency_code": "USD", + "value": "1500.00" + }, + "payout_status": { + "status": "COMPLETED" + }, + "create_time": "2018-06-29T18:38:38.000Z" + }, + "links": [ + { + "href": "https://stage2ma112402.pp-devqa-userstages.us-central1.gcp.dev.paypalinc.com:14084/v1/notifications/webhooks-events/WH-29H04174VA171590Y-15T39252MK897181P", + "rel": "self", + "method": "GET" + }, + { + "href": "https://stage2ma112402.pp-devqa-userstages.us-central1.gcp.dev.paypalinc.com:14084/v1/notifications/webhooks-events/WH-29H04174VA171590Y-15T39252MK897181P/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e4e04a*Host=ccg13amqunphttpdeliveryd34", + "client_pid": "651891", + "correlation-id": "0b53b22b6f914", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "83684f70-ba1e-11ed-a5f7-a56ac798bf6f", + "paypal-transmission-sig": "B7LpyBWXRUNrNeXoSh2TWNz0g1A0/qk1ggSljcrh/uPuhyaK+FEsXp6FqLzI3E+QY2/bsZF1Z75XBJiij2o8Ga+bMpFnmUV58vntJEzhCaC1D3SyL1Dag4pHwT3LkIogUJQMOubsWcu/qeGW2JE53hzfIFxFBvbxDEfYb3hHInHDwaYRBcZKV0zsyL7HmXNbZ19p1Iz/sQaNfP43dsl4q7nvMZBNaqj9znXIRHhoVwxJys2eNTB/LdD5xjIy73bp6VczQMCx9+TCdvhTiYR37p1oIUwZQ+MUHo9yw3O/YENAVUnIoH810FCos9v8qFX+2TGtyzN3+22yKMK6N61f6A==", + "paypal-transmission-time": "2023-03-03T23:52:53Z", + "x-b3-spanid": "75de47b3374e37be" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "MERCHANT.ONBOARDING.COMPLETED", + "name": "MERCHANT.ONBOARDING.COMPLETED", + "body": { + "id": "WH-2WR32451HC0233532-67976317FL4543714", + "create_time": "2016-08-01T21:41:28Z", + "resource_type": "merchant-onboarding", + "event_type": "MERCHANT.ONBOARDING.COMPLETED", + "resource": { + "merchant_id": "ELAMYJUN78D6G", + "partner_client_id": "AdZ3f8z-zIFM6t6Yilxvk3q4Iwx4ITRBjGR07q9wPJWms0z3G4ohajp7LdvpaLCeVLBmp5aVQ717YIw0", + "links": [ + { + "href": "https://uri.paypal.com/v1/customer/partners/QX4A9KG89BHLN/merchant-integrations/Y7MGFXVS8VHYU", + "rel": "self", + "method": "GET", + "description": "Get the merchant status information of merchants onboarded by this partner" + } + ] + } + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e4bdf4*Host=ccg13amqunphttpdeliveryd6", + "client_pid": "167487", + "correlation-id": "f41634d47ba6b", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "7f2c2c60-ba1e-11ed-ad84-09d4771a2e1a", + "paypal-transmission-sig": "QLnHj4AabUr3S/3yGP0/otWAJddFGhocASxIF57FsdST2O6517uCWpDR9JOw7HIwngITPGz4bYhxjI8+P9RLEiOUA/pR/Enj4eDkMD4qgM4Mo57apPtPcjdj5h7y1CmuIMbJjTC8weXW26YHkZAs5GooZnPRBR5xm8cRqWzwmuCVAdhu1WU3J6W8WrJvX3mCRDZn8oyoJuIXc0XK87imeeqWmYBh9mLqG2Au9MeYOD+eMilQ69t2eOonB9kWqsmoz5kgDxTCHxj/44JMaX4L0wwBbBJaN18nOEznVehrkwd6lmrA0a4ElCWjqlP2csjZdyZyKLjeQDDZ5hbyKpKhKw==", + "paypal-transmission-time": "2023-03-03T23:52:46Z", + "x-b3-spanid": "a1f179b009e5afdd" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "PAYMENT.SALE.COMPLETED", + "name": "PAYMENT.SALE.COMPLETED", + "body": { + "id": "WH-2WR32451HC0233532-67976317FL4543714", + "event_version": "1.0", + "create_time": "2014-10-23T17:23:52Z", + "resource_type": "sale", + "event_type": "PAYMENT.SALE.COMPLETED", + "summary": "A successful sale payment was made for $ 0.48 USD", + "resource": { + "id": "80021663DE681814L", + "create_time": "2014-10-23T17:22:56Z", + "update_time": "2014-10-23T17:23:04Z", + "amount": { + "total": "0.48", + "currency": "USD" + }, + "payment_mode": "ECHECK", + "state": "completed", + "protection_eligibility": "ELIGIBLE", + "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE", + "clearing_time": "2014-10-30T07:00:00Z", + "parent_payment": "PAY-1PA12106FU478450MKRETS4A", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/sale/80021663DE681814L", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/payments/sale/80021663DE681814L/refund", + "rel": "refund", + "method": "POST" + }, + { + "href": "https://api.paypal.com/v1/payments/payment/PAY-1PA12106FU478450MKRETS4A", + "rel": "parent_payment", + "method": "GET" + } + ] + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-2WR32451HC0233532-67976317FL4543714", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-2WR32451HC0233532-67976317FL4543714/resend", + "rel": "resend", + "method": "POST" + } + ] + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e5b092*Host=ccg13amqunphttpdeliveryd65", + "client_pid": "19218", + "correlation-id": "90ca05ca559c5", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "8dc4e5f0-ba1e-11ed-981f-630f4c7537a9", + "paypal-transmission-sig": "SuRXs4DIW2/Tnt9DbbinkNGIpnjT6OnLrmCOAZNzSy6CHnBK+D8D5GX8Mtkww8Wh5MbYTqVXfsck0s8x6zxSi6jvsW4DG80PK/OAp4u9waJclS76swwWZgGXv/DS0/mPePkPj3bhQwDuhdIqv7CU08EVD1QirMRUuKfLaFOqp6kE7RfZQhGx3orkU6wrYVdIJ/dy3nVHJ03esMQBh46eKiTM19ZiYXEp2mriDMXOKuiTZBZM3qIZCXU8ggqhGF41B56qZlRaR5UeKUF/eTDBNeBNUvPh3rp0biol1vz4i0Ujg5UCtuH/cfqnGA7rFs7Sju5DkIOdRzutxXRxcpPPTA==", + "paypal-transmission-time": "2023-03-03T23:53:10Z", + "x-b3-spanid": "1ae9c8fed03065c5" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "PAYMENTS.PAYMENT.CREATED", + "name": "PAYMENTS.PAYMENT.CREATED", + "body": { + "id": "WH-6W4482673W002281V-61985753LP2332451", + "event_type": "PAYMENTS.PAYMENT.CREATED", + "resource_type": "payment", + "summary": "Checkout payment is created and approved by buyer", + "create_time": "2015-05-11T21:45:15Z", + "resource": { + "id": "PAY-13V79659LS5126423KVISFPI", + "intent": "sale", + "update_time": "2019-01-25T00:12:08Z", + "payer": { + "payment_method": "paypal", + "status": "UNVERIFIED", + "payer_info": { + "email": "customer@example.com", + "first_name": "John", + "last_name": "Doe", + "payer_id": "MKPXKVHEHVCYU", + "shipping_address": { + "recipient_name": "John Doe", + "line1": "2211 N. 1st Street", + "city": "San Jose", + "state": "CA", + "postal_code": "95131", + "country_code": "US" + }, + "country_code": "US" + } + }, + "transactions": [ + { + "amount": { + "total": "4.13", + "currency": "EUR", + "details": { + "subtotal": "4.00", + "tax": "0.13", + "handling_fee": "0.00" + } + }, + "payee": { + "merchant_id": "2WVGFDAU8TB78", + "email": "merchant@example.com" + }, + "description": "This is a PayPal transaction", + "item_list": { + "shipping_address": { + "recipient_name": "John Doe", + "line1": "2211 N. 1st Street", + "city": "San Jose", + "state": "CA", + "postal_code": "95131", + "country_code": "US" + } + }, + "related_resources": [] + } + ], + "create_time": "2019-01-25T00:11:05Z", + "links": [ + { + "href": "https://api.paypal.com/v1/payments/PAY-13V79659LS5126423KVISFPI", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/payments/PAY-13V79659LS5126423KVISFPI/execute", + "rel": "execute", + "method": "POST" + }, + { + "href": "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-6BY44209VL795183H", + "rel": "approval_url", + "method": "REDIRECT" + } + ], + "state": "created" + }, + "links": [ + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-6W4482673W002281V-61985753LP2332451", + "rel": "self", + "method": "GET", + "encType": "application/json" + }, + { + "href": "https://api.paypal.com/v1/notifications/webhooks-events/WH-6W4482673W002281V-61985753LP2332451/resend", + "rel": "resend", + "method": "POST", + "encType": "application/json" + } + ], + "event_version": "1.0" + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e5be3e*Host=ccg14amqunphttpdeliveryd57", + "client_pid": "762903", + "correlation-id": "7fe32506e782e", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "9190e210-ba1e-11ed-bff1-7d56760e7310", + "paypal-transmission-sig": "P9mnBYeDanD0MPvHL6pCa5bGNkFBRZ6qp8ziPsNUZippw9NgpBg1M5ZKeGf8fCcSHe12MIBFTeKB6ZzaErB6wE2kpbFX5XvmMV+DkgyNBDxZo6S2DYaWmQ/VQVKGyzW1MAhJSMrWBxvawK9NAlSZgZgbz8/IlbyEtAbHBo22pfRUveGIOSQU1GBAqRcMNsyPYBkg3R4JoCzFjKEK7bMhOqj0RqecaZlq/olU3+7ekOuU2359b2yx4Ayih8m74UE/nGtMHgrEDlmsbnEQcx4qIMIohps8odwt60WMEOGxBN1Pgx4nstHzAq/hF272l7paxr73KkeRBQ7Y6npWO7iYeQ==", + "paypal-transmission-time": "2023-03-03T23:53:17Z", + "x-b3-spanid": "81f79d7bad2c8162" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "TAXES.REPORTS.DELIVERED", + "name": "TAXES.REPORTS.DELIVERED", + "body": { + "id": "WH-COC11055RA711503B-4YM959094A144403T", + "event_type": "TAXES.REPORTS.DELIVERED", + "resource_type": "TAX-REPORT", + "resource": { + "id": "11EB-45D4-5A0062D0-90A2-64639F99100A", + "tenant_name": "VENMO", + "fiscal_year_ending_date": "2020-12-31", + "total_segment": 1, + "mode": "INITIAL", + "status": "GENERATION_COMPLETED", + "configuration": { + "report_config_group_id": "1006", + "report_config_id": "1009", + "major_type": "1099K", + "minor_type": "FEDERAL", + "version_number": "1.0", + "report_name": "SUMMARY", + "tenant_name": "VENMO" + }, + "segments": [ + { + "id": "11EB-45D4-6298B412-8DF2-2C2C7CA71900", + "sequence": 1, + "name": "95530618_1099K_2020_1688337239197326490_1608744006975.pdf", + "source_type": "DMS", + "links": [ + { + "href": "/v2/content/documents/CMP-044-4b23279f-58e4-4dd8-bf5e-8c267ea3ed0f/files/1/download", + "rel": "segment", + "method": "GET" + } + ] + } + ], + "recipients": [ + { + "user": { + "id": "11EB-45D4-5A0062D1-B71C-C0DCE3E4E435", + "external_id": "1688337239197326490", + "tenant_name": "VENMO", + "status": "DELIVERY_COMPLETED", + "segments": [ + { + "id": "11EB-45D4-6298B412-8DF2-2C2C7CA71900", + "status": "DELIVERY_COMPLETED" + }, + { + "id": "11EB-45D4-6298B412-8DF2-2C2C7CA71700", + "status": "DELIVERY_COMPLETED" + } + ] + } + } + ], + "links": [ + { + "href": "/v1/taxes/reports/11EB-45D4-5A0062D0-90A2-64639F99100A", + "rel": "self", + "method": "GET" + } + ] + } + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e5b7ea*Host=ccg14amqunphttpdeliveryd8", + "client_pid": "764546", + "correlation-id": "47d06fdd8ac92", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "9802f7f0-ba1e-11ed-bc3e-fb8dfbb8a217", + "paypal-transmission-sig": "DsTwmth/uGwsaGqvpuhpeOwMI5CNJZMVHgDIhjj4jOcJWYMGI6d2Q7PtfXOXYn8tV7X5niJIq+VSt+i8jCHZiPTU53JQYAKX6A+T8HvqMKHl6mavnHy6y58hRhEVmYonpx2nqteAT2A6MQSPrOo0n0U7dVf77zxx+VDCMV3lqiGOaj4RSWOnzFOPKgnGXffFE2szoIYHVOok91Hp9s33+oKiNRZKiMPhu5ACzKd8lNvnQhMUIHEwndqJrhPXNpBGs0g8HjA4a7/cKQebAmN5seGMe2LPYUZD0/ProXC4xDVq6fXywGnan1uXzjbEsJQxIQedbhAON3VVpvvvUTIxMw==", + "paypal-transmission-time": "2023-03-03T23:53:28Z", + "x-b3-spanid": "cbe311a1b6310de9" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "paypal", + "providerLabel": "Paypal", + "eventType": "VAULT.CREDIT-CARD.CREATED", + "name": "VAULT.CREDIT-CARD.CREATED", + "body": { + "id": "WH-2WR32451HC0233532-67976317FL4543714", + "create_time": "2015-07-20T21:41:28Z", + "resource_type": "vault-credit-card", + "event_type": "VAULT.CREDIT-CARD.CREATED", + "summary": "A credit card was created", + "resource": { + "id ": "CARD - 2 XR99321JH288614BK7SF2ZY ", + "state ": "ok ", + "type ": "amex ", + "number ": "xxxxxxxxxxx4009 ", + "expire_month ": "7 ", + "expire_year ": "2027 ", + "first_name ": "Cool ", + "last_name ": "Buyer ", + "billing_address ": { + "line1 ": "2211 N First Street ", + "city ": "San Jose ", + "state ": "CA ", + "postal_code ": "95131 ", + "country_code ": "US " + }, + "valid_until ": "2019 - 09 - 22 T00: 00: 00 Z ", + "create_time ": "2016 - 09 - 22 T22: 38: 31 Z ", + "update_time ": "2016 - 09 - 22 T22: 38: 31 Z ", + "links ": [ + { + "href ": "https: //api.paypal.com/v1/vault/credit-card/CARD-2XR99321JH288614BK7SF2ZY", + "rel": "self", + "method": "GET" + }, + { + "href": "https://api.paypal.com/v1/vault/credit-card/CARD-2XR99321JH288614BK7SF2ZY", + "rel": "delete", + "method": "DELETE" + }, + { + "href": "https://api.paypal.com/v1/vault/credit-card/CARD-2XR99321JH288614BK7SF2ZY", + "rel": "patch", + "method": "PATCH" + } + ] + } + }, + "extraHeaders": { + "cal_poolstack": "amqunphttpdeliveryd:UNPHTDLVRYHA*CalThreadId=0*TopLevelTxnStartTime=186a9e2972c*Host=ccg14amqunphttpdeliveryd58", + "client_pid": "764671", + "correlation-id": "0fcc2887789f9", + "paypal-auth-algo": "SHA256withRSA", + "paypal-auth-version": "v2", + "paypal-cert-url": "https://api.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-38317689", + "paypal-transmission-id": "1ac27450-ba1e-11ed-a8c3-d774a01be652", + "paypal-transmission-sig": "LIoxCCBxrbnePbTRsdba4DZDxEWsWOcM48f2wH0g9mnjCxQS4F8LQLzsJuHy109JTtBz1+9Vn33IjFmJeyxCNbFlG5mc7O+PkMg76Bi2ERYHzkEycB9+J+QS/ttX2Jhvs1EEepqOiJapFadWt+y8d3C6uSBAVG5pnjMeuLgbyXXh2FeqoXeA5GmZotQETBrwLDfjDdUue+sB/OnAxIQgVBeQ0pO+nGrfHPsIV4WzJCxQc0F+tQRskWG/5+8dCXamQQOJkjuDW9RZhOpWpV3vIuczjSdh2I+1wfg6uMjHkpN4piejzm4da/Mta+cdeceZO31BVHgWQJ5+nX0vebODGw==", + "paypal-transmission-time": "2023-03-03T23:49:57Z", + "x-b3-spanid": "a0a43c9ef64b55e" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:paypal/1.0", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "added.activity", + "name": "added.activity", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 2, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "activity", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841422, + "timestamp_micro": 1680841422482110, + "prepublish_timestamp": 1680841422652, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "activity_notifications_language": null, + "send_activity_notifications": false, + "webhook_id": "174632" + }, + "current": { + "last_notification_time": null, + "location_street_number": null, + "type_name": "Call", + "reference_id": null, + "location_route": null, + "public_description": "", + "notification_language_id": null, + "subject": "Call", + "updates_story_id": null, + "type": "call", + "project_id": null, + "deal_title": null, + "original_start_time": null, + "id": 2, + "deal_id": null, + "busy_flag": false, + "person_id": 3, + "owner_name": "Hook Write", + "attendees": null, + "person_name": "Anthony Edwards", + "project_title": null, + "rec_rule_extension": null, + "done": false, + "created_by_user_id": 14542087, + "location_sublocality": null, + "rec_rule": null, + "location_admin_area_level_2": null, + "user_id": 14542087, + "location_admin_area_level_1": null, + "org_id": 2, + "conference_meeting_client": null, + "note": null, + "due_time": "", + "rec_master_activity_id": null, + "location_country": null, + "active_flag": true, + "duration": "", + "location_postal_code": null, + "update_time": "2023-04-07 04:23:42", + "update_user_id": null, + "lead_title": null, + "source_timezone": null, + "person_dropbox_bcc": "hookwrite@pipedrivemail.com", + "conference_meeting_id": null, + "org_name": "NoCo Solutions", + "location_locality": null, + "assigned_to_user_id": 14542087, + "lead_id": null, + "is_recurring": null, + "participants": [ + { + "primary_flag": true, + "person_id": 3 + } + ], + "location_subpremise": null, + "company_id": 11149162, + "due_date": "2023-04-07", + "lead": null, + "reference_type": null, + "last_notification_user_id": null, + "calendar_sync_include_context": null, + "marked_as_done_time": "", + "location_formatted_address": null, + "series": null, + "conference_meeting_url": null, + "location": null, + "deal_dropbox_bcc": null, + "add_time": "2023-04-07 04:23:42" + }, + "previous": null, + "event": "added.activity", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "added.deal", + "name": "added.deal", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 1, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "deal", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680840600, + "timestamp_micro": 1680840600460452, + "prepublish_timestamp": 1680840600800, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "email_messages_count": 0, + "cc_email": "hookwrite+deal1@pipedrivemail.com", + "products_count": 0, + "next_activity_date": null, + "next_activity_type": null, + "next_activity_duration": null, + "id": 1, + "person_id": 3, + "creator_user_id": 14542087, + "expected_close_date": "2023-05-30", + "owner_name": "Hook Write", + "participants_count": 0, + "stage_id": 1, + "probability": null, + "undone_activities_count": 0, + "renewal_type": "one_time", + "active": true, + "person_name": "Anthony Edwards", + "last_activity_date": null, + "close_time": null, + "org_hidden": false, + "next_activity_id": null, + "weighted_value_currency": "XAF", + "stage_order_nr": 0, + "next_activity_subject": null, + "rotten_time": null, + "user_id": 14542087, + "visible_to": "3", + "org_id": 2, + "notes_count": 0, + "next_activity_time": null, + "formatted_value": "FCFA 2,000,000", + "status": "open", + "formatted_weighted_value": "FCFA 2,000,000", + "first_won_time": null, + "last_outgoing_mail_time": null, + "title": "NoCo Solutions deal", + "last_activity_id": null, + "update_time": "2023-04-07 04:10:00", + "activities_count": 0, + "pipeline_id": 1, + "lost_time": null, + "currency": "XAF", + "weighted_value": 2000000, + "org_name": "NoCo Solutions", + "value": 2000000, + "person_hidden": false, + "next_activity_note": null, + "files_count": 0, + "last_incoming_mail_time": null, + "label": null, + "lost_reason": null, + "deleted": false, + "won_time": null, + "followers_count": 0, + "stage_change_time": null, + "add_time": "2023-04-07 04:10:00", + "done_activities_count": 0 + }, + "previous": null, + "event": "added.deal", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "added.organization", + "name": "added.organization", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 2, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "organization", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680840510, + "timestamp_micro": 1680840510932610, + "prepublish_timestamp": 1680840511087, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "address_route": null, + "related_closed_deals_count": 0, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 0, + "people_count": 0, + "next_activity_date": null, + "id": 2, + "address_admin_area_level_2": null, + "address_admin_area_level_1": null, + "owner_name": "Hook Write", + "address_formatted_address": null, + "related_won_deals_count": 0, + "undone_activities_count": 0, + "address_subpremise": null, + "last_activity_date": null, + "next_activity_id": null, + "country_code": null, + "visible_to": "3", + "notes_count": 0, + "name": "NoCo Solutions", + "address_locality": null, + "lost_deals_count": 0, + "next_activity_time": null, + "delete_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": null, + "update_time": "2023-04-07 04:08:30", + "category_id": null, + "activities_count": 0, + "won_deals_count": 0, + "address_street_number": null, + "address": null, + "edit_name": true, + "files_count": 0, + "company_id": 11149162, + "address_postal_code": null, + "address_country": null, + "first_char": "n", + "closed_deals_count": 0, + "label": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "followers_count": 0, + "address_sublocality": null, + "add_time": "2023-04-07 04:08:30", + "done_activities_count": 0 + }, + "previous": null, + "event": "added.organization", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "added.person", + "name": "added.person", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 3, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "person", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680840511, + "timestamp_micro": 1680840511329055, + "prepublish_timestamp": 1680840512203, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "related_closed_deals_count": 0, + "delete_time": null, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 0, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": null, + "next_activity_date": null, + "update_time": "2023-04-07 04:08:31", + "participant_open_deals_count": 0, + "activities_count": 0, + "id": 3, + "org_name": "NoCo Solutions", + "first_name": "Anthony", + "email": [ + { + "label": "work", + "value": "anthon@noco.com", + "primary": true + } + ], + "won_deals_count": 0, + "owner_name": "Hook Write", + "files_count": 0, + "company_id": 11149162, + "related_won_deals_count": 0, + "last_incoming_mail_time": null, + "first_char": "a", + "undone_activities_count": 0, + "closed_deals_count": 0, + "last_name": "Edwards", + "last_activity_date": null, + "label": 5, + "next_activity_id": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "phone": [ + { + "label": "work", + "value": "+1434993473", + "primary": true + } + ], + "visible_to": "3", + "org_id": 2, + "notes_count": 0, + "followers_count": 0, + "name": "Anthony Edwards", + "participant_closed_deals_count": 0, + "primary_email": "anthon@noco.com", + "lost_deals_count": 0, + "next_activity_time": null, + "add_time": "2023-04-07 04:08:31", + "done_activities_count": 0 + }, + "previous": null, + "event": "added.person", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "added.product", + "name": "added.product", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "added", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 1, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "product", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841164, + "timestamp_micro": 1680841164749820, + "prepublish_timestamp": 1680841164880, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "code": "00023", + "files_count": null, + "owner_id": 14542087, + "first_char": "s", + "selectable": true, + "description": null, + "tax": 0, + "active_flag": true, + "unit": "233", + "update_time": "2023-04-07 04:19:24", + "visible_to": "3", + "followers_count": 0, + "name": "Survey consultancy", + "product_variations": [], + "id": 1, + "prices": [ + { + "cost": 0, + "price": 300000, + "product_id": 1, + "overhead_cost": 0, + "currency": "XAF", + "id": 1, + "price_formatted": "XAF300,000" + } + ], + "category": "15", + "add_time": "2023-04-07 04:19:24" + }, + "previous": null, + "event": "added.product", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "deleted.activity", + "name": "deleted.activity", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "deleted", + "change_source": "api", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 2, + "is_bulk_update": true, + "matches_filters": { + "current": [] + }, + "object": "activity", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841432, + "timestamp_micro": 1680841432612516, + "prepublish_timestamp": 1680841432685, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": null, + "previous": { + "last_notification_time": null, + "location_street_number": null, + "type_name": "Call", + "reference_id": null, + "location_route": null, + "public_description": "", + "notification_language_id": null, + "subject": "Call", + "type": "call", + "project_id": null, + "deal_title": null, + "original_start_time": null, + "id": 2, + "deal_id": null, + "busy_flag": false, + "person_id": 3, + "owner_name": "Hook Write", + "attendees": null, + "person_name": "Anthony Edwards", + "project_title": null, + "rec_rule_extension": null, + "done": false, + "created_by_user_id": 14542087, + "location_sublocality": null, + "rec_rule": null, + "location_admin_area_level_2": null, + "user_id": 14542087, + "location_admin_area_level_1": null, + "org_id": 2, + "conference_meeting_client": null, + "note": null, + "due_time": "", + "rec_master_activity_id": null, + "location_country": null, + "active_flag": true, + "duration": "", + "location_postal_code": null, + "update_time": "2023-04-07 04:23:42", + "update_user_id": null, + "lead_title": null, + "source_timezone": null, + "person_dropbox_bcc": "hookwrite@pipedrivemail.com", + "conference_meeting_id": null, + "org_name": "NoCo Solutions", + "location_locality": null, + "assigned_to_user_id": 14542087, + "lead_id": null, + "is_recurring": null, + "participants": [ + { + "primary_flag": true, + "person_id": 3 + } + ], + "location_subpremise": null, + "company_id": 11149162, + "due_date": "2023-04-07", + "lead": null, + "reference_type": null, + "last_notification_user_id": null, + "calendar_sync_include_context": null, + "marked_as_done_time": "", + "location_formatted_address": null, + "series": null, + "conference_meeting_url": null, + "location": null, + "deal_dropbox_bcc": null, + "add_time": "2023-04-07 04:23:42" + }, + "event": "deleted.activity", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "deleted.person", + "name": "deleted.person", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "deleted", + "change_source": "api", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 2, + "is_bulk_update": true, + "matches_filters": { + "current": [] + }, + "object": "person", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841339, + "timestamp_micro": 1680841339437193, + "prepublish_timestamp": 1680841339479, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": null, + "previous": { + "related_closed_deals_count": 0, + "delete_time": null, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 0, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": null, + "next_activity_date": null, + "update_time": "2023-04-07 04:09:19", + "participant_open_deals_count": 0, + "activities_count": 0, + "id": 2, + "org_name": "Demi", + "first_name": "Oluwa", + "email": [ + { + "label": "work", + "value": "magnify@mail.com", + "primary": true + } + ], + "won_deals_count": 0, + "files_count": 0, + "company_id": 11149162, + "related_won_deals_count": 0, + "last_incoming_mail_time": null, + "first_char": "o", + "undone_activities_count": 0, + "closed_deals_count": 0, + "last_name": "Tomi", + "picture_128_url": null, + "last_activity_date": null, + "label": null, + "next_activity_id": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "phone": [ + { + "label": "work", + "value": "+125433437", + "primary": true + } + ], + "visible_to": "3", + "org_id": 1, + "notes_count": 0, + "followers_count": 1, + "name": "Oluwa Tomi", + "participant_closed_deals_count": 0, + "lost_deals_count": 0, + "next_activity_time": null, + "add_time": "2023-04-06 19:24:38", + "done_activities_count": 0 + }, + "event": "deleted.person", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "updated.activity", + "name": "updated.activity", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 1, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "activity", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841419, + "timestamp_micro": 1680841419576234, + "prepublish_timestamp": 1680841419764, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "activity_notifications_language": null, + "send_activity_notifications": false, + "webhook_id": "174632" + }, + "current": { + "last_notification_time": null, + "location_street_number": null, + "type_name": "Call", + "reference_id": null, + "location_route": null, + "public_description": "", + "notification_language_id": null, + "subject": "Call", + "type": "call", + "project_id": null, + "deal_title": null, + "original_start_time": null, + "id": 1, + "deal_id": null, + "busy_flag": false, + "person_id": 3, + "owner_name": "Hook Write", + "attendees": null, + "person_name": "Anthony Edwards", + "project_title": null, + "rec_rule_extension": null, + "done": true, + "created_by_user_id": 14542087, + "location_sublocality": null, + "rec_rule": null, + "location_admin_area_level_2": null, + "user_id": 14542087, + "location_admin_area_level_1": null, + "org_id": 2, + "conference_meeting_client": null, + "note": null, + "due_time": "", + "rec_master_activity_id": null, + "location_country": null, + "active_flag": true, + "duration": "", + "location_postal_code": null, + "update_time": "2023-04-07 04:23:39", + "update_user_id": 14542087, + "lead_title": null, + "source_timezone": null, + "person_dropbox_bcc": "hookwrite@pipedrivemail.com", + "conference_meeting_id": null, + "org_name": "NoCo Solutions", + "location_locality": null, + "assigned_to_user_id": 14542087, + "lead_id": null, + "is_recurring": null, + "participants": [ + { + "primary_flag": true, + "person_id": 3 + } + ], + "location_subpremise": null, + "company_id": 11149162, + "due_date": "2023-04-07", + "lead": null, + "reference_type": null, + "last_notification_user_id": null, + "calendar_sync_include_context": null, + "marked_as_done_time": "2023-04-07 04:23:39", + "location_formatted_address": null, + "series": null, + "conference_meeting_url": null, + "location": null, + "deal_dropbox_bcc": null, + "add_time": "2023-04-07 04:23:25" + }, + "previous": { + "last_notification_time": null, + "local_sorting_due_date": null, + "location_street_number": null, + "type_name": "Call", + "reference_id": null, + "location_route": null, + "public_description": "", + "notification_language_id": null, + "subject": "Call", + "type": "call", + "project_id": null, + "deal_title": null, + "original_start_time": null, + "activity_note": null, + "id": 1, + "deal_id": null, + "busy_flag": false, + "person_id": 3, + "owner_name": "Hook Write", + "attendees": null, + "person_name": "Anthony Edwards", + "project_title": null, + "rec_rule_extension": null, + "done": false, + "created_by_user_id": 14542087, + "location_sublocality": null, + "rec_rule": null, + "activityNote": null, + "location_admin_area_level_2": null, + "user_id": 14542087, + "location_admin_area_level_1": null, + "org_id": 2, + "conference_meeting_client": null, + "note": null, + "due_time": "", + "rec_master_activity_id": null, + "location_country": null, + "active_flag": true, + "duration": "", + "location_postal_code": null, + "update_time": "2023-04-07 04:23:25", + "service_reference": [], + "update_user_id": null, + "lead_title": null, + "source_timezone": null, + "person_dropbox_bcc": "hookwrite@pipedrivemail.com", + "conference_meeting_id": null, + "org_name": "NoCo Solutions", + "location_locality": null, + "assigned_to_user_id": 14542087, + "lead_id": null, + "is_recurring": null, + "participants": [ + { + "primary_flag": true, + "person_id": 3 + } + ], + "location_subpremise": null, + "company_id": 11149162, + "org": null, + "due_date": "2023-04-07", + "lead": null, + "reference_type": null, + "last_notification_user_id": null, + "calendar_sync_include_context": null, + "marked_as_done_time": "", + "location_formatted_address": null, + "series": null, + "conference_meeting_url": null, + "location": null, + "deal_dropbox_bcc": null, + "user": { + "created": null, + "last_login": null, + "has_pic": null, + "active_flag": true, + "locale": null, + "pic_hash": null, + "update_time": "2023-04-06 18:05:40", + "role_id": null, + "name": "Hook Write", + "timezone_name": null, + "modified": null, + "id": 14542087, + "default_currency": null, + "add_time": "2023-04-06 18:05:40", + "email": null + }, + "add_time": "2023-04-07 04:23:25" + }, + "event": "updated.activity", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "updated.deal", + "name": "updated.deal", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "app", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 1, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "deal", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680840600, + "timestamp_micro": 1680840600882126, + "prepublish_timestamp": 1680840601081, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "email_messages_count": 0, + "cc_email": "hookwrite+deal1@pipedrivemail.com", + "products_count": 0, + "next_activity_date": null, + "next_activity_type": null, + "next_activity_duration": null, + "id": 1, + "person_id": 3, + "creator_user_id": 14542087, + "expected_close_date": "2023-05-30", + "owner_name": "Hook Write", + "participants_count": 1, + "stage_id": 1, + "probability": null, + "undone_activities_count": 0, + "renewal_type": "one_time", + "active": true, + "person_name": "Anthony Edwards", + "last_activity_date": null, + "close_time": null, + "org_hidden": false, + "next_activity_id": null, + "weighted_value_currency": "XAF", + "stage_order_nr": 0, + "next_activity_subject": null, + "rotten_time": null, + "user_id": 14542087, + "visible_to": "3", + "org_id": 2, + "notes_count": 0, + "next_activity_time": null, + "formatted_value": "FCFA 2,000,000", + "status": "open", + "formatted_weighted_value": "FCFA 2,000,000", + "first_won_time": null, + "last_outgoing_mail_time": null, + "title": "NoCo Solutions deal", + "last_activity_id": null, + "update_time": "2023-04-07 04:10:00", + "last_activity": null, + "next_activity": null, + "activities_count": 0, + "pipeline_id": 1, + "lost_time": null, + "currency": "XAF", + "weighted_value": 2000000, + "org_name": "NoCo Solutions", + "value": 2000000, + "person_hidden": false, + "next_activity_note": null, + "files_count": 0, + "last_incoming_mail_time": null, + "label": null, + "lost_reason": null, + "deleted": false, + "won_time": null, + "followers_count": 1, + "stage_change_time": null, + "add_time": "2023-04-07 04:10:00", + "done_activities_count": 0 + }, + "previous": { + "email_messages_count": 0, + "cc_email": "hookwrite+deal1@pipedrivemail.com", + "products_count": 0, + "next_activity_date": null, + "next_activity_type": null, + "next_activity_duration": null, + "id": 1, + "person_id": 3, + "creator_user_id": 14542087, + "expected_close_date": "2023-05-30", + "owner_name": "Hook Write", + "participants_count": 0, + "stage_id": 1, + "probability": null, + "undone_activities_count": 0, + "renewal_type": "one_time", + "active": true, + "person_name": "Anthony Edwards", + "last_activity_date": null, + "close_time": null, + "org_hidden": false, + "next_activity_id": null, + "weighted_value_currency": "XAF", + "stage_order_nr": 0, + "next_activity_subject": null, + "rotten_time": null, + "user_id": 14542087, + "visible_to": "3", + "org_id": 2, + "notes_count": 0, + "next_activity_time": null, + "formatted_value": "FCFA 2,000,000", + "status": "open", + "formatted_weighted_value": "FCFA 2,000,000", + "first_won_time": null, + "last_outgoing_mail_time": null, + "title": "NoCo Solutions deal", + "last_activity_id": null, + "update_time": "2023-04-07 04:10:00", + "last_activity": null, + "next_activity": null, + "activities_count": 0, + "pipeline_id": 1, + "lost_time": null, + "currency": "XAF", + "weighted_value": 2000000, + "org_name": "NoCo Solutions", + "value": 2000000, + "person_hidden": false, + "next_activity_note": null, + "files_count": 0, + "last_incoming_mail_time": null, + "label": null, + "lost_reason": null, + "deleted": false, + "won_time": null, + "followers_count": 1, + "stage_change_time": null, + "add_time": "2023-04-07 04:10:00", + "done_activities_count": 0 + }, + "event": "updated.deal", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "updated.organization", + "name": "updated.organization", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "api", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 2, + "is_bulk_update": true, + "matches_filters": { + "current": [] + }, + "object": "organization", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841432, + "timestamp_micro": 1680841432637031, + "prepublish_timestamp": 1680841432700, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "address_route": null, + "related_closed_deals_count": 0, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 1, + "people_count": 1, + "next_activity_date": null, + "id": 2, + "address_admin_area_level_2": null, + "address_admin_area_level_1": null, + "owner_name": "Hook Write", + "address_formatted_address": null, + "related_won_deals_count": 0, + "undone_activities_count": 0, + "address_subpremise": null, + "last_activity_date": "2023-04-07", + "next_activity_id": null, + "country_code": null, + "visible_to": "3", + "notes_count": 0, + "name": "NoCo Solutions", + "address_locality": null, + "lost_deals_count": 0, + "next_activity_time": null, + "delete_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": 1, + "update_time": "2023-04-07 04:23:52", + "category_id": null, + "activities_count": 1, + "won_deals_count": 0, + "address_street_number": null, + "address": null, + "edit_name": true, + "files_count": 0, + "company_id": 11149162, + "address_postal_code": null, + "address_country": null, + "first_char": "n", + "closed_deals_count": 0, + "label": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "followers_count": 1, + "address_sublocality": null, + "add_time": "2023-04-07 04:08:30", + "done_activities_count": 1 + }, + "previous": { + "address_route": null, + "related_closed_deals_count": 0, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 1, + "people_count": 1, + "next_activity_date": "2023-04-07", + "id": 2, + "address_admin_area_level_2": null, + "address_admin_area_level_1": null, + "owner_name": "Hook Write", + "address_formatted_address": null, + "related_won_deals_count": 0, + "undone_activities_count": 1, + "address_subpremise": null, + "last_activity_date": "2023-04-07", + "next_activity_id": 2, + "country_code": null, + "visible_to": "3", + "notes_count": 0, + "name": "NoCo Solutions", + "address_locality": null, + "lost_deals_count": 0, + "next_activity_time": null, + "delete_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": 1, + "update_time": "2023-04-07 04:23:42", + "category_id": null, + "activities_count": 2, + "won_deals_count": 0, + "address_street_number": null, + "address": null, + "edit_name": true, + "files_count": 0, + "company_id": 11149162, + "address_postal_code": null, + "address_country": null, + "first_char": "n", + "closed_deals_count": 0, + "label": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "followers_count": 1, + "address_sublocality": null, + "add_time": "2023-04-07 04:08:30", + "done_activities_count": 1 + }, + "event": "updated.organization", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "updated.person", + "name": "updated.person", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "api", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 3, + "is_bulk_update": true, + "matches_filters": { + "current": [] + }, + "object": "person", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841432, + "timestamp_micro": 1680841432652693, + "prepublish_timestamp": 1680841432827, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "related_closed_deals_count": 0, + "delete_time": null, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 1, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": 1, + "next_activity_date": null, + "update_time": "2023-04-07 04:23:52", + "participant_open_deals_count": 0, + "activities_count": 1, + "id": 3, + "org_name": "NoCo Solutions", + "first_name": "Anthony", + "email": [ + { + "label": "work", + "value": "anthon@noco.com", + "primary": true + } + ], + "won_deals_count": 0, + "owner_name": "Hook Write", + "files_count": 0, + "company_id": 11149162, + "related_won_deals_count": 0, + "last_incoming_mail_time": null, + "first_char": "a", + "undone_activities_count": 0, + "closed_deals_count": 0, + "last_name": "Edwards", + "last_activity_date": "2023-04-07", + "label": 5, + "next_activity_id": null, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "phone": [ + { + "label": "work", + "value": "+1434993473", + "primary": true + } + ], + "visible_to": "3", + "org_id": 2, + "notes_count": 0, + "followers_count": 1, + "name": "Anthony Edwards", + "participant_closed_deals_count": 0, + "primary_email": "anthon@noco.com", + "lost_deals_count": 0, + "next_activity_time": null, + "add_time": "2023-04-07 04:08:31", + "done_activities_count": 1 + }, + "previous": { + "related_closed_deals_count": 0, + "delete_time": null, + "email_messages_count": 0, + "cc_email": "hookwrite@pipedrivemail.com", + "owner_id": 14542087, + "open_deals_count": 1, + "last_outgoing_mail_time": null, + "active_flag": true, + "picture_id": null, + "last_activity_id": 1, + "next_activity_date": "2023-04-07", + "update_time": "2023-04-07 04:23:42", + "participant_open_deals_count": 0, + "activities_count": 2, + "id": 3, + "org_name": "NoCo Solutions", + "first_name": "Anthony", + "email": [ + { + "label": "work", + "value": "anthon@noco.com", + "primary": true + } + ], + "won_deals_count": 0, + "owner_name": "Hook Write", + "files_count": 0, + "company_id": 11149162, + "related_won_deals_count": 0, + "last_incoming_mail_time": null, + "first_char": "a", + "undone_activities_count": 1, + "closed_deals_count": 0, + "last_name": "Edwards", + "last_activity_date": "2023-04-07", + "label": 5, + "next_activity_id": 2, + "related_lost_deals_count": 0, + "related_open_deals_count": 0, + "phone": [ + { + "label": "work", + "value": "+1434993473", + "primary": true + } + ], + "visible_to": "3", + "org_id": 2, + "notes_count": 0, + "followers_count": 1, + "name": "Anthony Edwards", + "participant_closed_deals_count": 0, + "primary_email": "anthon@noco.com", + "lost_deals_count": 0, + "next_activity_time": null, + "add_time": "2023-04-07 04:08:31", + "done_activities_count": 1 + }, + "event": "updated.person", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "pipedrive", + "providerLabel": "Pipedrive", + "eventType": "updated.product", + "name": "updated.product", + "body": { + "v": 1, + "matches_filters": { + "current": [] + }, + "meta": { + "action": "updated", + "change_source": "api", + "company_id": 11149162, + "host": "hookwrite.pipedrive.com", + "id": 1, + "is_bulk_update": false, + "matches_filters": { + "current": [] + }, + "object": "product", + "permitted_user_ids": [ + 14542087 + ], + "pipedrive_service_name": false, + "timestamp": 1680841165, + "timestamp_micro": 1680841165016475, + "prepublish_timestamp": 1680841165047, + "trans_pending": false, + "user_id": 14542087, + "v": 1, + "webhook_id": "174632" + }, + "current": { + "code": "00023", + "files_count": null, + "company_id": 11149162, + "owner_id": 14542087, + "first_char": "s", + "selectable": true, + "description": null, + "tax": 0, + "active_flag": true, + "unit": "233", + "update_time": "2023-04-07 04:19:25", + "visible_to": "3", + "followers_count": 1, + "name": "Survey consultancy", + "product_variations": [], + "id": 1, + "prices": [ + { + "cost": 0, + "price": 300000, + "product_id": 1, + "overhead_cost": 0, + "currency": "XAF", + "id": 1 + } + ], + "category": "15", + "add_time": "2023-04-07 04:19:24" + }, + "previous": { + "code": "00023", + "files_count": null, + "owner_id": 14542087, + "first_char": "s", + "selectable": true, + "description": null, + "tax": 0, + "active_flag": true, + "unit": "233", + "update_time": "2023-04-07 04:19:24", + "visible_to": "3", + "followers_count": 0, + "name": "Survey consultancy", + "product_variations": [], + "id": 1, + "prices": [ + { + "cost": 0, + "price": 300000, + "product_id": 1, + "overhead_cost": 0, + "currency": "XAF", + "id": 1 + } + ], + "category": "15", + "add_time": "2023-04-07 04:19:24" + }, + "event": "updated.product", + "retry": 0 + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:pipedrive/1", + "snapshotDate": "2026-07" + } + }, + { + "provider": "scrapfly", + "providerLabel": "Scrapfly", + "eventType": "extraction", + "name": "extraction", + "body": { + "content_type": "application/json", + "data": { + "price": "$19.99", + "product_name": "Widget", + "stock": "In stock" + } + }, + "extraHeaders": { + "x-scrapfly-webhook-env": "LIVE", + "x-scrapfly-webhook-job-id": "571b3e08-4575-4072-9b68-90c9df8ac542", + "x-scrapfly-webhook-name": "samples-capture", + "x-scrapfly-webhook-project": "default", + "x-scrapfly-webhook-resource-type": "extraction" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:scrapfly/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "scrapfly", + "providerLabel": "Scrapfly", + "eventType": "scrape", + "name": "scrape", + "body": { + "config": { + "asp": false, + "auto_scroll": false, + "body": null, + "browser_brand": null, + "cache": false, + "cache_clear": false, + "cache_ttl": 86400, + "correlation_id": null, + "cost_budget": null, + "country": null, + "debug": false, + "dns": false, + "enable_backend_profile": false, + "enable_cdp_trace": false, + "env": "LIVE", + "extract": null, + "extraction_model": null, + "extraction_model_custom_schema": null, + "extraction_prompt": null, + "extraction_template": null, + "fingerprint_uuid": null, + "format": "raw", + "geolocation": null, + "headers": {}, + "js": null, + "js_scenario": null, + "lang": null, + "log_eviction_date": "2026-05-22 12:56:27", + "method": "GET", + "origin": "WEBHOOK", + "os": null, + "project": "default", + "project_uuid": "34f3bdea-4991-41c9-8923-3fd8a1e31895", + "proxy_pool": "public_datacenter_pool", + "render_js": false, + "rendering_stage": "", + "rendering_wait": 0, + "request_id": "69f331ae-a6d3-4f03-a8d7-d7c93e05fcde", + "retry": true, + "schedule_name": null, + "screenshot_flags": null, + "screenshot_resolution": null, + "screenshots": null, + "session": null, + "session_sticky_proxy": true, + "ssl": false, + "tags": null, + "timeout": 150000, + "url": "https://web-scraping.dev/products", + "uuid": "01KRNVB97E6313HTXH6XCHQE2G", + "vision_deficiency": null, + "wait_for_selector": null, + "webhook_name": "samples-capture", + "job_uuid": "5eb1468b-029f-46c9-b402-1466607818af" + }, + "context": { + "asp": null, + "cache": { + "entry": null, + "state": "MISS" + }, + "cookies": [], + "cost": { + "details": [ + { + "amount": 1, + "code": "PROXY_DATACENTER_NETWORK", + "description": "Proxy Network (Datacenter)" + } + ], + "total": 1 + }, + "created_at": "2026-05-15 12:56:27.732886", + "debug": null, + "env": "LIVE", + "headers": {}, + "is_xml_http_request": false, + "job": { + "uuid": "5eb1468b-029f-46c9-b402-1466607818af", + "created_at": "2026-05-15 12:56:25.084000", + "origin": "WEB_SCRAPING_API", + "type": "SCRAPE", + "status": "DONE", + "is_finished": true, + "is_success": true, + "webhook_uuid": "ac5fecce-20f0-4351-8410-d93c8be75ddb", + "schedule_uuid": null, + "crawler_uuid": null, + "spider_uuid": null, + "duration": 4.29, + "finished_at": "2026-05-15 12:56:29.377017", + "not_before": null, + "last_status_updated_at": "2026-05-15 12:56:29.377117", + "metadata": {} + }, + "lang": "en", + "optimizations": [], + "os": { + "name": "mac", + "version": "26.3.0" + }, + "project": "default", + "proxy": { + "country": "ph", + "identity": "552f7b60", + "network": "datacenter", + "pool": "public_datacenter_pool" + }, + "redirects": [], + "retry": 0, + "schedule": null, + "session": null, + "spider": null, + "throttler": null, + "uri": { + "base_url": "https://web-scraping.dev", + "fragment": null, + "host": "web-scraping.dev", + "params": null, + "port": 443, + "query": null, + "root_domain": "web-scraping.dev", + "scheme": "https" + }, + "url": "https://web-scraping.dev/products", + "webhook": { + "consecutive_failed_count": 0, + "disable_reason": null, + "max_concurrency": null, + "name": "samples-capture", + "queue_limit": 500, + "queued_scrape_order": 3, + "webhook_uuid": "ac5fecce-20f0-4351-8410-d93c8be75ddb" + }, + "bandwidth_consumed": 0 + }, + "insights": null, + "result": { + "ai_snapshot": null, + "browser_data": { + "attachments": [], + "javascript_evaluation_result": null, + "js_scenario": null, + "local_storage_data": {}, + "local_storage_deletions": {}, + "private_token_data": null, + "session_storage_data": {}, + "session_storage_deletions": {}, + "websockets": [], + "xhr_call": [] + }, + "content": "\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n\n\nweb-scraping.dev product page 1\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n \n \n \n \n
\n \n
\n
\n all\n apparel\n consumables\n household\n
\n \n
\n
\n
\n
\n \n \n
\n
\n \"thumbnail\n
\n
\n

Box of Chocolate Candy

\n
Indulge your sweet tooth with our Box of Chocolate Candy. Each box contains an assortment of rich, flavorful chocolates with a smooth, creamy filling. Choose from a variety of flavors including zesty orange and sweet cherry. Whether you're looking for the perfect gift or just want to treat yourself, our Box of Chocolate Candy is sure to satisfy.
\n
\n
\n
24.99
\n
\n
\n\n \n \n
\n
\n \"thumbnail\n
\n
\n

Dark Red Energy Potion

\n
Unleash the power within with our 'Dark Red Potion', an energy drink as intense as the games you play. Its deep red color and bold cherry cola flavor are as inviting as they are invigorating. Bring out the best in your gaming performance, and unlock your full potential.
\n
\n
\n
4.99
\n
\n
\n\n \n \n
\n
\n \"thumbnail\n
\n
\n

Teal Energy Potion

\n
Experience a surge of vitality with our 'Teal Potion', an exceptional energy drink designed for the gaming community. With its intriguing teal color and a flavor that keeps you asking for more, this potion is your best companion during those long gaming nights. Every sip is an adventure - let the quest begin!
\n
\n
\n
4.99
\n
\n
\n\n \n \n
\n
\n \"thumbnail\n
\n
\n

Red Energy Potion

\n
Elevate your game with our 'Red Potion', an extraordinary energy drink that's as enticing as it is effective. This fiery red potion delivers an explosive berry flavor and an energy kick that keeps you at the top of your game. Are you ready to level up?
\n
\n
\n
4.99
\n
\n
\n\n \n \n
\n
\n \"thumbnail\n
\n
\n

Blue Energy Potion

\n
Ignite your gaming sessions with our 'Blue Energy Potion', a premium energy drink crafted for dedicated gamers. Inspired by the classic video game potions, this energy drink provides a much-needed boost to keep you focused and energized. It's more than just an energy drink - it's an ode to the gaming culture, packaged in an aesthetically pleasing potion-like bottle that'll make you feel like you're in your favorite game world. Drink up and game on!
\n
\n
\n
4.99
\n
\n
\n\n \n
\n
page 1 of total 28 results in 6 pages
\n
\n <\n \n \n 1\n \n \n \n 2\n \n \n \n 3\n \n \n \n 4\n \n \n \n 5\n \n \n \n \n >\n
\n
\n
\n\n
\n \n\n
\n \n
\n \n\n\n \n", + "content_encoding": "utf-8", + "content_format": "raw", + "content_type": "text/html; charset=utf-8", + "cookies": [], + "data": null, + "dns": null, + "duration": 1.64, + "error": null, + "extracted_data": null, + "format": "text", + "iframes": [], + "log_url": "https://scrapfly.io/dashboard/monitoring/log/01KRNVB97E6313HTXH6XCHQE2G", + "reason": "OK", + "request_headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-encoding": "gzip, deflate, br, zstd", + "accept-language": "en-PH,en;q=0.9,en-US;q=0.8", + "priority": "u=0, i", + "sec-ch-ua": "\"Chromium\";v=\"148\", \"Google Chrome\";v=\"148\", \"Not/A)Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"", + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" + }, + "response_headers": { + "content-length": "17153", + "content-type": "text/html; charset=utf-8", + "date": "Fri, 15 May 2026 12:56:28 GMT", + "permissions-policy": "fullscreen=(self), autoplay=*, geolocation=(), camera=(), ch-ua=*, ch-ua-arch=*, ch-ua-bitness=*, ch-ua-full-version=*, ch-ua-full-version-list=*, ch-ua-mobile=*, ch-ua-model=*, ch-ua-platform=*, ch-ua-platform-version=*, ch-ua-wow64=*, ch-ua-form-factor=*, ch-viewport-width=*, ch-viewport-height=*, ch-dpr=*, ch-width=*, ch-prefers-reduced-motion=*, ch-prefers-color-scheme=*, ch-downlink=*, ch-ect=*, ch-rtt=*, ch-save-data=*, ch-device-memory=*", + "referrer-policy": "strict-origin-when-cross-origin", + "server": "uvicorn", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "x-content-type-options": "nosniff", + "x-xss-protection": "1; mode=block" + }, + "screenshots": {}, + "size": 0, + "ssl": null, + "status": "DONE", + "status_code": 200, + "success": true, + "url": "https://web-scraping.dev/products" + }, + "uuid": "01KRNVB97E6313HTXH6XCHQE2G" + }, + "extraHeaders": { + "x-scrapfly-log-url": "https://scrapfly.io/dashboard/monitoring/log/01KRNVB97E6313HTXH6XCHQE2G", + "x-scrapfly-log-uuid": "01KRNVB97E6313HTXH6XCHQE2G", + "x-scrapfly-webhook-env": "LIVE", + "x-scrapfly-webhook-job-id": "5eb1468b-029f-46c9-b402-1466607818af", + "x-scrapfly-webhook-name": "samples-capture", + "x-scrapfly-webhook-project": "default", + "x-scrapfly-webhook-resource-type": "scrape" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:scrapfly/latest", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "app_purchases_one_time/update", + "name": "app_purchases_one_time/update", + "body": { + "app_purchase_one_time": { + "admin_graphql_api_id": "gid://shopify/AppPurchaseOneTime/1017262349", + "name": "Webhook Test", + "status": "PENDING", + "admin_graphql_api_shop_id": "gid://shopify/Shop/548380009", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "app_purchases_one_time/update", + "x-shopify-triggered-at": "2024-08-07T22:48:08.320603914Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "app_subscriptions/approaching_capped_amount", + "name": "app_subscriptions/approaching_capped_amount", + "body": { + "app_subscription": { + "admin_graphql_api_id": "gid://shopify/AppSubscription/1029266948", + "name": "Webhook Test", + "balance_used": 0, + "capped_amount": "20.0", + "currency_code": "USD", + "admin_graphql_api_shop_id": "gid://shopify/Shop/548380009", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "app_subscriptions/approaching_capped_amount", + "x-shopify-triggered-at": "2024-08-07T22:48:17.296500484Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "app_subscriptions/update", + "name": "app_subscriptions/update", + "body": { + "app_subscription": { + "admin_graphql_api_id": "gid://shopify/AppSubscription/1029266952", + "name": "Webhook Test", + "status": "PENDING", + "admin_graphql_api_shop_id": "gid://shopify/Shop/548380009", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "capped_amount": "20.0" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "app_subscriptions/update", + "x-shopify-triggered-at": "2024-08-07T22:48:22.376601190Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "app/uninstalled", + "name": "app/uninstalled", + "body": { + "id": 548380009, + "name": "Super Toys", + "email": "super@supertoys.com", + "domain": null, + "province": "Tennessee", + "country": "US", + "address1": "190 MacLaren Street", + "zip": "37178", + "city": "Houston", + "source": null, + "phone": "3213213210", + "latitude": null, + "longitude": null, + "primary_locale": "en", + "address2": null, + "created_at": null, + "updated_at": null, + "country_code": "US", + "country_name": "United States", + "currency": "USD", + "customer_email": "super@supertoys.com", + "timezone": "(GMT-05:00) Eastern Time (US & Canada)", + "iana_timezone": null, + "shop_owner": "John Smith", + "money_format": "${{amount}}", + "money_with_currency_format": "${{amount}} USD", + "weight_unit": "kg", + "province_code": "TN", + "taxes_included": null, + "auto_configure_tax_inclusivity": null, + "tax_shipping": null, + "county_taxes": null, + "plan_display_name": "Shopify Plus", + "plan_name": "enterprise", + "has_discounts": false, + "has_gift_cards": true, + "myshopify_domain": null, + "google_apps_domain": null, + "google_apps_login_enabled": null, + "money_in_emails_format": "${{amount}}", + "money_with_currency_in_emails_format": "${{amount}} USD", + "eligible_for_payments": true, + "requires_extra_payments_agreement": false, + "password_enabled": null, + "has_storefront": true, + "finances": true, + "primary_location_id": 655441491, + "checkout_api_supported": true, + "multi_location_enabled": true, + "setup_required": false, + "pre_launch_enabled": false, + "enabled_presentment_currencies": [ + "USD" + ], + "marketing_sms_consent_enabled_at_checkout": false, + "transactional_sms_disabled": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "app/uninstalled", + "x-shopify-triggered-at": "2024-08-07T22:48:27.480992003Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "attributed_sessions/first", + "name": "attributed_sessions/first", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "attributed_sessions/first", + "x-shopify-triggered-at": "2024-08-07T22:48:32.635829834Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "attributed_sessions/last", + "name": "attributed_sessions/last", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "attributed_sessions/last", + "x-shopify-triggered-at": "2024-08-07T22:48:37.645452488Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "audit_events/admin_api_activity", + "name": "audit_events/admin_api_activity", + "body": { + "events": [ + { + "time": 1634678724, + "event": { + "context": { + "context_type": "network_request", + "context_identifier": "13493be9-24f2-4eea-b71d-a8acd7af7917", + "context_metadata": { + "client_ip": "192.168.64.1", + "content_type": "json", + "response_time_ms": 116.47000000812113, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36", + "api_request_failed": false, + "response_code": 200 + } + }, + "action": "api_request", + "actor": { + "actor_type": "app", + "actor_identifier": 4, + "actor_metadata": { + "app_name": "Private app", + "api_version_requested": "unstable", + "api_version_served": "unstable", + "ecosystem_category": "private" + }, + "on_behalf_of": { + "user_email": "dev@example.com" + } + }, + "subject": { + "subject_type": "shop", + "subject_identifier": "shop1.myshopify.io", + "subject_metadata": {} + }, + "timestamp": "2021-10-19T21:25:24Z", + "additional_metadata": { + "request_type": "GraphQL", + "mutation_names": [], + "query": "query orders {\n orders(first: $first) {\n edges {\n node {\n id\n }\n }\n }\n}", + "variables": { + "first": 10 + }, + "error_codes": [] + } + } + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "audit_events/admin_api_activity", + "x-shopify-triggered-at": "2024-08-07T22:48:42.842783143Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "bulk_operations/finish", + "name": "bulk_operations/finish", + "body": { + "admin_graphql_api_id": "gid://shopify/BulkOperation/147595010", + "completed_at": "2024-07-29T11:59:46-04:00", + "created_at": "2024-07-29T11:59:46-04:00", + "error_code": null, + "status": "completed", + "type": "query" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "bulk_operations/finish", + "x-shopify-triggered-at": "2024-08-07T22:48:48.382830812Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "carts/create", + "name": "carts/create", + "body": { + "id": "eeafa272cebfd4b22385bc4b645e762c", + "token": "eeafa272cebfd4b22385bc4b645e762c", + "line_items": [ + { + "id": 704912205188288500, + "properties": {}, + "quantity": 3, + "variant_id": 704912205188288500, + "key": "704912205188288575:33f11f7a1ec7d93b826de33bb54de37b", + "discounted_price": "19.99", + "discounts": [], + "gift_card": false, + "grams": 200, + "line_price": "59.97", + "original_line_price": "59.97", + "original_price": "19.99", + "price": "19.99", + "product_id": 788032119674292900, + "sku": "example-shirt-s", + "taxable": true, + "title": "Example T-Shirt - Small", + "total_discount": "0.00", + "vendor": "Acme", + "discounted_price_set": { + "shop_money": { + "amount": "19.99", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "19.99", + "currency_code": "USD" + } + }, + "line_price_set": { + "shop_money": { + "amount": "59.97", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "59.97", + "currency_code": "USD" + } + }, + "original_line_price_set": { + "shop_money": { + "amount": "59.97", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "59.97", + "currency_code": "USD" + } + }, + "price_set": { + "shop_money": { + "amount": "19.99", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "19.99", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.0", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.0", + "currency_code": "USD" + } + } + } + ], + "note": null, + "updated_at": "2022-01-01T00:00:00.000Z", + "created_at": "2022-01-01T00:00:00.000Z" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "carts/create", + "x-shopify-triggered-at": "2024-08-07T22:48:53.450036423Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "carts/update", + "name": "carts/update", + "body": { + "id": "eeafa272cebfd4b22385bc4b645e762c", + "token": "eeafa272cebfd4b22385bc4b645e762c", + "line_items": [ + { + "id": 704912205188288500, + "properties": {}, + "quantity": 3, + "variant_id": 704912205188288500, + "key": "704912205188288575:33f11f7a1ec7d93b826de33bb54de37b", + "discounted_price": "19.99", + "discounts": [], + "gift_card": false, + "grams": 200, + "line_price": "59.97", + "original_line_price": "59.97", + "original_price": "19.99", + "price": "19.99", + "product_id": 788032119674292900, + "sku": "example-shirt-s", + "taxable": true, + "title": "Example T-Shirt - Small", + "total_discount": "0.00", + "vendor": "Acme", + "discounted_price_set": { + "shop_money": { + "amount": "19.99", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "19.99", + "currency_code": "USD" + } + }, + "line_price_set": { + "shop_money": { + "amount": "59.97", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "59.97", + "currency_code": "USD" + } + }, + "original_line_price_set": { + "shop_money": { + "amount": "59.97", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "59.97", + "currency_code": "USD" + } + }, + "price_set": { + "shop_money": { + "amount": "19.99", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "19.99", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.0", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.0", + "currency_code": "USD" + } + } + } + ], + "note": null, + "updated_at": "2022-01-01T00:00:00.000Z", + "created_at": "2022-01-01T00:00:00.000Z" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "carts/update", + "x-shopify-triggered-at": "2024-08-07T22:48:58.525199459Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "channels/delete", + "name": "channels/delete", + "body": { + "id": "123456789" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "channels/delete", + "x-shopify-triggered-at": "2024-08-07T22:49:03.887622994Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "checkouts/create", + "name": "checkouts/create", + "body": { + "id": 981820079255243500, + "token": "123123123", + "cart_token": "eeafa272cebfd4b22385bc4b645e762c", + "email": "example@email.com", + "gateway": null, + "buyer_accepts_marketing": false, + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "landing_site": null, + "note": null, + "note_attributes": [], + "referring_site": null, + "shipping_lines": [], + "taxes_included": false, + "total_weight": 1133, + "currency": "USD", + "completed_at": null, + "closed_at": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "device_id": null, + "phone": null, + "customer_locale": null, + "line_items": [ + { + "applied_discounts": [], + "discount_allocations": [], + "key": "0fede25d9590a4b164cd8391fc839d37", + "destination_location_id": 1015975136, + "fulfillment_service": "manual", + "gift_card": false, + "grams": 567, + "origin_location_id": 1015975135, + "presentment_title": "IPod Nano - 8GB", + "presentment_variant_title": "Pink", + "product_id": 632910392, + "properties": null, + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "tax_lines": [ + { + "price": "11.94", + "position": 1, + "rate": 0.06, + "title": "Tax", + "source": "Shopify", + "zone": "province", + "jurisdiction_id": null, + "jurisdiction_type": null, + "jurisdiction_source": null, + "reporting_taxable_amount": null, + "reporting_non_taxable_amount": null, + "reporting_exempt_amount": null, + "reporting_jurisdiction_name": null, + "reporting_jurisdiction_type": null, + "reporting_jurisdiction_code": null, + "tax_api_client_id": null, + "tax_calculation_price": "11.94", + "tax_registration_id": null, + "compare_at": 0.06, + "channel_liable": false + } + ], + "taxable": true, + "title": "IPod Nano - 8GB", + "variant_id": 808950810, + "variant_title": "Pink", + "variant_price": "199.00", + "vendor": "Apple", + "user_id": null, + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "rank": null, + "compare_at_price": null, + "line_price": "199.00", + "price": "199.00" + }, + { + "applied_discounts": [], + "discount_allocations": [], + "key": "0fede25d9590a4b164cd8391fc839d37", + "destination_location_id": 1015975136, + "fulfillment_service": "manual", + "gift_card": false, + "grams": 567, + "origin_location_id": 1015975135, + "presentment_title": "IPod Nano - 8GB", + "presentment_variant_title": "Pink", + "product_id": 632910392, + "properties": null, + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "tax_lines": [ + { + "price": "11.94", + "position": 1, + "rate": 0.06, + "title": "Tax", + "source": "Shopify", + "zone": "province", + "jurisdiction_id": null, + "jurisdiction_type": null, + "jurisdiction_source": null, + "reporting_taxable_amount": null, + "reporting_non_taxable_amount": null, + "reporting_exempt_amount": null, + "reporting_jurisdiction_name": null, + "reporting_jurisdiction_type": null, + "reporting_jurisdiction_code": null, + "tax_api_client_id": null, + "tax_calculation_price": "11.94", + "tax_registration_id": null, + "compare_at": 0.06, + "channel_liable": false + } + ], + "taxable": true, + "title": "IPod Nano - 8GB", + "variant_id": 808950810, + "variant_title": "Pink", + "variant_price": "199.00", + "vendor": "Apple", + "user_id": null, + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "rank": null, + "compare_at_price": null, + "line_price": "199.00", + "price": "199.00" + } + ], + "name": "#981820079255243537", + "source": null, + "abandoned_checkout_url": "https://checkout.local/548380009/checkouts/123123123/recover?key=example-secret-token", + "discount_codes": [], + "tax_lines": [ + { + "price": "23.88", + "rate": 0.06, + "title": "Tax", + "channel_liable": false + } + ], + "source_name": "web", + "presentment_currency": "USD", + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "total_discounts": "0.00", + "total_line_items_price": "398.00", + "total_price": "421.88", + "total_tax": "23.88", + "subtotal_price": "398.00", + "total_duties": null, + "reservation_token": null, + "billing_address": { + "first_name": "Bob", + "address1": "123 Billing Street", + "phone": "555-555-BILL", + "city": "Billtown", + "zip": "K2P0B0", + "province": "Kentucky", + "country": "United States", + "last_name": "Biller", + "address2": null, + "company": "My Company", + "latitude": null, + "longitude": null, + "name": "Bob Biller", + "country_code": "US", + "province_code": "KY" + }, + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "K2P0S0", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 603851970716743400, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "tax_exemptions": [], + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/603851970716743426", + "default_address": { + "id": null, + "customer_id": 603851970716743400, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "checkouts/create", + "x-shopify-triggered-at": "2024-08-07T22:49:09.417931715Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "checkouts/delete", + "name": "checkouts/delete", + "body": { + "id": 981820079255243500, + "presentment_currency": "USD", + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "total_discounts": "0.00", + "total_line_items_price": "398.00", + "total_price": "421.88", + "total_tax": "23.88", + "subtotal_price": "398.00", + "cart_token": "eeafa272cebfd4b22385bc4b645e762c", + "total_duties": null, + "reservation_token": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "checkouts/delete", + "x-shopify-triggered-at": "2024-08-07T22:49:14.518371058Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "checkouts/update", + "name": "checkouts/update", + "body": { + "id": 981820079255243500, + "token": "123123123", + "cart_token": "eeafa272cebfd4b22385bc4b645e762c", + "email": "example@email.com", + "gateway": null, + "buyer_accepts_marketing": false, + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "landing_site": null, + "note": null, + "note_attributes": [], + "referring_site": null, + "shipping_lines": [], + "taxes_included": false, + "total_weight": 1133, + "currency": "USD", + "completed_at": null, + "closed_at": null, + "user_id": null, + "location_id": null, + "source_identifier": null, + "source_url": null, + "device_id": null, + "phone": null, + "customer_locale": null, + "line_items": [ + { + "applied_discounts": [], + "discount_allocations": [], + "key": "04146c0fca55b1ad72ae9ed35248b684", + "destination_location_id": 1015975152, + "fulfillment_service": "manual", + "gift_card": false, + "grams": 567, + "origin_location_id": 1015975151, + "presentment_title": "IPod Nano - 8GB", + "presentment_variant_title": "Pink", + "product_id": 632910392, + "properties": null, + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "tax_lines": [ + { + "price": "11.94", + "position": 1, + "rate": 0.06, + "title": "Tax", + "source": "Shopify", + "zone": "province", + "jurisdiction_id": null, + "jurisdiction_type": null, + "jurisdiction_source": null, + "reporting_taxable_amount": null, + "reporting_non_taxable_amount": null, + "reporting_exempt_amount": null, + "reporting_jurisdiction_name": null, + "reporting_jurisdiction_type": null, + "reporting_jurisdiction_code": null, + "tax_api_client_id": null, + "tax_calculation_price": "11.94", + "tax_registration_id": null, + "compare_at": 0.06, + "channel_liable": false + } + ], + "taxable": true, + "title": "IPod Nano - 8GB", + "variant_id": 808950810, + "variant_title": "Pink", + "variant_price": "199.00", + "vendor": "Apple", + "user_id": null, + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "rank": null, + "compare_at_price": null, + "line_price": "199.00", + "price": "199.00" + }, + { + "applied_discounts": [], + "discount_allocations": [], + "key": "04146c0fca55b1ad72ae9ed35248b684", + "destination_location_id": 1015975152, + "fulfillment_service": "manual", + "gift_card": false, + "grams": 567, + "origin_location_id": 1015975151, + "presentment_title": "IPod Nano - 8GB", + "presentment_variant_title": "Pink", + "product_id": 632910392, + "properties": null, + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "tax_lines": [ + { + "price": "11.94", + "position": 1, + "rate": 0.06, + "title": "Tax", + "source": "Shopify", + "zone": "province", + "jurisdiction_id": null, + "jurisdiction_type": null, + "jurisdiction_source": null, + "reporting_taxable_amount": null, + "reporting_non_taxable_amount": null, + "reporting_exempt_amount": null, + "reporting_jurisdiction_name": null, + "reporting_jurisdiction_type": null, + "reporting_jurisdiction_code": null, + "tax_api_client_id": null, + "tax_calculation_price": "11.94", + "tax_registration_id": null, + "compare_at": 0.06, + "channel_liable": false + } + ], + "taxable": true, + "title": "IPod Nano - 8GB", + "variant_id": 808950810, + "variant_title": "Pink", + "variant_price": "199.00", + "vendor": "Apple", + "user_id": null, + "unit_price_measurement": { + "measured_type": null, + "quantity_value": null, + "quantity_unit": null, + "reference_value": null, + "reference_unit": null + }, + "rank": null, + "compare_at_price": null, + "line_price": "199.00", + "price": "199.00" + } + ], + "name": "#981820079255243537", + "source": null, + "abandoned_checkout_url": "https://checkout.local/548380009/checkouts/123123123/recover?key=example-secret-token", + "discount_codes": [], + "tax_lines": [ + { + "price": "23.88", + "rate": 0.06, + "title": "Tax", + "channel_liable": false + } + ], + "source_name": "web", + "presentment_currency": "USD", + "buyer_accepts_sms_marketing": false, + "sms_marketing_phone": null, + "total_discounts": "0.00", + "total_line_items_price": "398.00", + "total_price": "421.88", + "total_tax": "23.88", + "subtotal_price": "398.00", + "total_duties": null, + "reservation_token": null, + "billing_address": { + "first_name": "Bob", + "address1": "123 Billing Street", + "phone": "555-555-BILL", + "city": "Billtown", + "zip": "K2P0B0", + "province": "Kentucky", + "country": "United States", + "last_name": "Biller", + "address2": null, + "company": "My Company", + "latitude": null, + "longitude": null, + "name": "Bob Biller", + "country_code": "US", + "province_code": "KY" + }, + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "K2P0S0", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 603851970716743400, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "tax_exemptions": [], + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/603851970716743426", + "default_address": { + "id": null, + "customer_id": 603851970716743400, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "checkouts/update", + "x-shopify-triggered-at": "2024-08-07T22:49:23.634640521Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collection_listings/add", + "name": "collection_listings/add", + "body": { + "collection_listing": { + "collection_id": 408372092144951400, + "updated_at": null, + "body_html": "Some HTML", + "default_product_image": null, + "handle": "mynewcollection", + "image": null, + "title": "My New Collection", + "sort_order": null, + "published_at": "2021-12-31T19:00:00-05:00" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collection_listings/add", + "x-shopify-triggered-at": "2024-08-07T22:49:44.862206549Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collection_listings/remove", + "name": "collection_listings/remove", + "body": { + "collection_listing": { + "collection_id": 408372092144951400 + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collection_listings/remove", + "x-shopify-triggered-at": "2024-08-07T22:49:50.146358330Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collection_listings/update", + "name": "collection_listings/update", + "body": { + "collection_listing": { + "collection_id": 408372092144951400, + "updated_at": null, + "body_html": "Some HTML", + "default_product_image": null, + "handle": "mynewcollection", + "image": null, + "title": "My New Collection", + "sort_order": null, + "published_at": "2021-12-31T19:00:00-05:00" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collection_listings/update", + "x-shopify-triggered-at": "2024-08-07T22:49:55.707110165Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collection_publications/create", + "name": "collection_publications/create", + "body": { + "id": null, + "publication_id": null, + "published_at": "2021-12-31T19:00:00-05:00", + "published": true, + "created_at": null, + "updated_at": null, + "collection_id": 408372092144951400 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collection_publications/create", + "x-shopify-triggered-at": "2024-08-07T22:50:00.831787789Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collection_publications/delete", + "name": "collection_publications/delete", + "body": { + "id": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collection_publications/delete", + "x-shopify-triggered-at": "2024-08-07T22:50:06.705687879Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collection_publications/update", + "name": "collection_publications/update", + "body": { + "id": null, + "publication_id": null, + "published_at": "2021-12-31T19:00:00-05:00", + "published": true, + "created_at": null, + "updated_at": null, + "collection_id": 408372092144951400 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collection_publications/update", + "x-shopify-triggered-at": "2024-08-07T22:50:12.004948018Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collections/create", + "name": "collections/create", + "body": { + "id": 408372092144951400, + "handle": "mynewcollection", + "title": "My New Collection", + "updated_at": "2021-12-31T19:00:00-05:00", + "body_html": "Some HTML", + "published_at": "2021-12-31T16:00:00-05:00", + "sort_order": null, + "template_suffix": null, + "published_scope": "web", + "admin_graphql_api_id": "gid://shopify/Collection/408372092144951419" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collections/create", + "x-shopify-triggered-at": "2024-08-07T22:49:29.263300969Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collections/delete", + "name": "collections/delete", + "body": { + "id": 408372092144951400, + "published_scope": "web", + "admin_graphql_api_id": "gid://shopify/Collection/408372092144951419" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collections/delete", + "x-shopify-triggered-at": "2024-08-07T22:49:34.453393728Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "collections/update", + "name": "collections/update", + "body": { + "id": 408372092144951400, + "handle": "mynewcollection", + "title": "My New Collection", + "updated_at": "2021-12-31T19:00:00-05:00", + "body_html": "Some HTML", + "published_at": "2021-12-31T16:00:00-05:00", + "sort_order": null, + "template_suffix": null, + "published_scope": "web", + "admin_graphql_api_id": "gid://shopify/Collection/408372092144951419" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "collections/update", + "x-shopify-triggered-at": "2024-08-07T22:49:39.400114567Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "companies/create", + "name": "companies/create", + "body": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "companies/create", + "x-shopify-triggered-at": "2024-08-07T22:50:17.121540889Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "companies/delete", + "name": "companies/delete", + "body": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "companies/delete", + "x-shopify-triggered-at": "2024-08-07T22:50:22.239198640Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "companies/update", + "name": "companies/update", + "body": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "companies/update", + "x-shopify-triggered-at": "2024-08-07T22:50:27.321014059Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_contact_roles/assign", + "name": "company_contact_roles/assign", + "body": { + "company_contact": { + "customer_admin_graphql_api_id": "gid://shopify/Customer/12123842227812391", + "title": "Buyer", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951419", + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + } + }, + "company_location": { + "name": "Montreal", + "external_id": "123456789", + "phone": "555-555-5555", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "note": "Head Office Location", + "buyer_experience_configuration": null, + "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", + "tax_exemptions": [ + "CA_BC_CONTRACTOR_EXEMPTION", + "CA_BC_RESELLER_EXEMPTION" + ], + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "billing_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "shipping_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "tax_registration": { + "tax_id": "1214214141" + } + }, + "company_contact_role": { + "name": "Location Admin" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_contact_roles/assign", + "x-shopify-triggered-at": "2024-08-07T22:50:48.208059762Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_contact_roles/revoke", + "name": "company_contact_roles/revoke", + "body": { + "company_contact": { + "customer_admin_graphql_api_id": "gid://shopify/Customer/12123842227812391", + "title": "Buyer", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951419", + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + } + }, + "company_location": { + "name": "Montreal", + "external_id": "123456789", + "phone": "555-555-5555", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "note": "Head Office Location", + "buyer_experience_configuration": null, + "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", + "tax_exemptions": [ + "CA_BC_CONTRACTOR_EXEMPTION", + "CA_BC_RESELLER_EXEMPTION" + ], + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "billing_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "shipping_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "tax_registration": { + "tax_id": "1214214141" + } + }, + "company_contact_role": { + "name": "Location Admin" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_contact_roles/revoke", + "x-shopify-triggered-at": "2024-08-07T22:50:53.245467153Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_contacts/create", + "name": "company_contacts/create", + "body": { + "customer_admin_graphql_api_id": "gid://shopify/Customer/12123842227812391", + "title": "Buyer", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951419", + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_contacts/create", + "x-shopify-triggered-at": "2024-08-07T22:50:32.566005428Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_contacts/delete", + "name": "company_contacts/delete", + "body": { + "customer_admin_graphql_api_id": "gid://shopify/Customer/12123842227812391", + "title": "Buyer", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951419", + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_contacts/delete", + "x-shopify-triggered-at": "2024-08-07T22:50:38.009787956Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_contacts/update", + "name": "company_contacts/update", + "body": { + "customer_admin_graphql_api_id": "gid://shopify/Customer/12123842227812391", + "title": "Buyer", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951419", + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_contacts/update", + "x-shopify-triggered-at": "2024-08-07T22:50:43.200340708Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_locations/create", + "name": "company_locations/create", + "body": { + "name": "Montreal", + "external_id": "123456789", + "phone": "555-555-5555", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "note": "Head Office Location", + "buyer_experience_configuration": null, + "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", + "tax_exemptions": [ + "CA_BC_CONTRACTOR_EXEMPTION", + "CA_BC_RESELLER_EXEMPTION" + ], + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "billing_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "shipping_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "tax_registration": { + "tax_id": "1214214141" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_locations/create", + "x-shopify-triggered-at": "2024-08-07T22:50:58.308549192Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_locations/delete", + "name": "company_locations/delete", + "body": { + "name": "Montreal", + "external_id": "123456789", + "phone": "555-555-5555", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "note": "Head Office Location", + "buyer_experience_configuration": null, + "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", + "tax_exemptions": [ + "CA_BC_CONTRACTOR_EXEMPTION", + "CA_BC_RESELLER_EXEMPTION" + ], + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "billing_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "shipping_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "tax_registration": { + "tax_id": "1214214141" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_locations/delete", + "x-shopify-triggered-at": "2024-08-07T22:51:03.577723567Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "company_locations/update", + "name": "company_locations/update", + "body": { + "name": "Montreal", + "external_id": "123456789", + "phone": "555-555-5555", + "locale": "en", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "note": "Head Office Location", + "buyer_experience_configuration": null, + "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", + "tax_exemptions": [ + "CA_BC_CONTRACTOR_EXEMPTION", + "CA_BC_RESELLER_EXEMPTION" + ], + "company": { + "name": "Example Company", + "note": "This is an example company", + "external_id": "123456789", + "main_contact_admin_graphql_api_id": "gid://shopify/CompanyContact/408372092144951652", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "customer_since": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "billing_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "shipping_address": { + "address1": "175 Sherbrooke Street West", + "city": "Montreal", + "province": "Quebec", + "country": "Canada", + "zip": "H3A 0G4", + "recipient": "Adam Felix", + "first_name": null, + "last_name": null, + "address2": null, + "phone": "+49738001239", + "zone_code": "QC", + "country_code": "CA", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/CompanyAddress/141016871799219115", + "company_admin_graphql_api_id": "gid://shopify/Company/408372092144951419" + }, + "tax_registration": { + "tax_id": "1214214141" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "company_locations/update", + "x-shopify-triggered-at": "2024-08-07T22:51:08.839449181Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_account_settings/update", + "name": "customer_account_settings/update", + "body": { + "url": null, + "customer_accounts_version": "classic", + "login_required_at_checkout": true, + "login_links_visible_on_storefront_and_checkout": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_account_settings/update", + "x-shopify-triggered-at": "2024-08-07T22:51:57.130836490Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_groups/create", + "name": "customer_groups/create", + "body": { + "id": 239443597569284770, + "name": "Bob Customers", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "query": "email:bob*" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_groups/create", + "x-shopify-triggered-at": "2024-08-07T22:52:02.519965885Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_groups/delete", + "name": "customer_groups/delete", + "body": { + "id": 239443597569284770 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_groups/delete", + "x-shopify-triggered-at": "2024-08-07T22:52:08.313033131Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_groups/update", + "name": "customer_groups/update", + "body": { + "id": 239443597569284770, + "name": "Bob Customers", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "query": "email:bob*" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_groups/update", + "x-shopify-triggered-at": "2024-08-07T22:52:13.505274530Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_payment_methods/create", + "name": "customer_payment_methods/create", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_payment_methods/create", + "x-shopify-triggered-at": "2024-08-07T22:52:18.980219438Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_payment_methods/revoke", + "name": "customer_payment_methods/revoke", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_payment_methods/revoke", + "x-shopify-triggered-at": "2024-08-07T22:52:24.088407291Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customer_payment_methods/update", + "name": "customer_payment_methods/update", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customer_payment_methods/update", + "x-shopify-triggered-at": "2024-08-07T22:52:29.279171485Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers_email_marketing_consent/update", + "name": "customers_email_marketing_consent/update", + "body": { + "customer_id": 706405506930370000, + "email_address": null, + "email_marketing_consent": { + "state": null, + "opt_in_level": null, + "consent_updated_at": null + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers_email_marketing_consent/update", + "x-shopify-triggered-at": "2024-08-07T22:51:30.617878896Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers_marketing_consent/update", + "name": "customers_marketing_consent/update", + "body": { + "id": 706405506930370000, + "phone": null, + "sms_marketing_consent": { + "state": null, + "opt_in_level": null, + "consent_updated_at": null, + "consent_collected_from": "other" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers_marketing_consent/update", + "x-shopify-triggered-at": "2024-08-07T22:51:41.050833512Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers/create", + "name": "customers/create", + "body": { + "id": 706405506930370000, + "email": "bob@biller.com", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "first_name": "Bob", + "last_name": "Biller", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": "This customer loves ice cream", + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "addresses": [], + "tax_exemptions": [], + "email_marketing_consent": null, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers/create", + "x-shopify-triggered-at": "2024-08-07T22:51:14.136738016Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers/delete", + "name": "customers/delete", + "body": { + "id": 706405506930370000, + "phone": null, + "addresses": [], + "tax_exemptions": [], + "email_marketing_consent": null, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers/delete", + "x-shopify-triggered-at": "2024-08-07T22:51:19.507506882Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers/disable", + "name": "customers/disable", + "body": { + "id": 706405506930370000, + "email": "bob@biller.com", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "first_name": "Bob", + "last_name": "Biller", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": "This customer loves ice cream", + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "addresses": [], + "tax_exemptions": [], + "email_marketing_consent": null, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers/disable", + "x-shopify-triggered-at": "2024-08-07T22:51:25.462040521Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers/enable", + "name": "customers/enable", + "body": { + "id": 706405506930370000, + "email": "bob@biller.com", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "first_name": "Bob", + "last_name": "Biller", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": "This customer loves ice cream", + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "addresses": [], + "tax_exemptions": [], + "email_marketing_consent": null, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers/enable", + "x-shopify-triggered-at": "2024-08-07T22:51:35.892845057Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers/merge", + "name": "customers/merge", + "body": { + "admin_graphql_api_customer_kept_id": "gid://shopify/Customer/1", + "admin_graphql_api_customer_deleted_id": "gid://shopify/Customer/2", + "admin_graphql_api_job_id": null, + "status": "failed", + "errors": [ + { + "customer_ids": [ + 1 + ], + "field": "merge_in_progress", + "message": "John Doe is currently being merged." + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers/merge", + "x-shopify-triggered-at": "2024-08-07T22:51:46.447952714Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "customers/update", + "name": "customers/update", + "body": { + "id": 706405506930370000, + "email": "bob@biller.com", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "first_name": "Bob", + "last_name": "Biller", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": "This customer loves ice cream", + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "addresses": [], + "tax_exemptions": [], + "email_marketing_consent": null, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "customers/update", + "x-shopify-triggered-at": "2024-08-07T22:51:51.872901866Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "discounts/create", + "name": "discounts/create", + "body": { + "admin_graphql_api_id": "gid://shopify/DiscountAutomaticNode/1", + "title": "Automatic free shipping", + "status": "ACTIVE", + "created_at": "2016-08-29T08:00:00-04:00", + "updated_at": "2016-08-29T08:00:00-04:00" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "discounts/create", + "x-shopify-triggered-at": "2024-08-07T22:52:34.528828230Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "discounts/delete", + "name": "discounts/delete", + "body": { + "admin_graphql_api_id": "gid://shopify/DiscountAutomaticNode/1", + "deleted_at": "2018-08-29T08:00:00-04:00" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "discounts/delete", + "x-shopify-triggered-at": "2024-08-07T22:52:39.895676196Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "discounts/redeemcode_added", + "name": "discounts/redeemcode_added", + "body": { + "admin_graphql_api_id": "gid://shopify/DiscountCodeNode/1", + "redeem_code": { + "id": "gid://shopify/DiscountRedeemCode/1", + "code": "code1" + }, + "updated_at": "2018-08-29T12:00:00.000Z" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "discounts/redeemcode_added", + "x-shopify-triggered-at": "2024-08-07T22:52:45.089981481Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "discounts/redeemcode_removed", + "name": "discounts/redeemcode_removed", + "body": { + "admin_graphql_api_id": "gid://shopify/DiscountCodeNode/1", + "redeem_code": { + "id": "gid://shopify/DiscountRedeemCode/1", + "code": "code1" + }, + "updated_at": "2018-08-29T12:00:00.000Z" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "discounts/redeemcode_removed", + "x-shopify-triggered-at": "2024-08-07T22:52:50.969357631Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "discounts/update", + "name": "discounts/update", + "body": { + "admin_graphql_api_id": "gid://shopify/DiscountAutomaticNode/1", + "title": "Automatic free shipping updated", + "status": "ACTIVE", + "created_at": "2016-08-29T08:00:00-04:00", + "updated_at": "2016-08-29T08:00:00-04:00" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "discounts/update", + "x-shopify-triggered-at": "2024-08-07T22:52:56.323408044Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "disputes/create", + "name": "disputes/create", + "body": { + "id": 285332461850802050, + "order_id": 820982911946154500, + "type": "chargeback", + "amount": "11.50", + "currency": "CAD", + "reason": "fraudulent", + "network_reason_code": "4837", + "status": "under_review", + "evidence_due_by": "2021-12-30T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2021-12-31T19:00:00-05:00" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "disputes/create", + "x-shopify-triggered-at": "2024-08-07T22:53:01.347294875Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "disputes/update", + "name": "disputes/update", + "body": { + "id": 285332461850802050, + "order_id": 820982911946154500, + "type": "chargeback", + "amount": "11.50", + "currency": "CAD", + "reason": "fraudulent", + "network_reason_code": "4837", + "status": "under_review", + "evidence_due_by": "2021-12-30T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2021-12-31T19:00:00-05:00" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "disputes/update", + "x-shopify-triggered-at": "2024-08-07T22:53:06.813323331Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "domains/create", + "name": "domains/create", + "body": { + "id": 690933842, + "host": "jsmith.myshopify.com", + "ssl_enabled": true, + "localization": { + "country": null, + "default_locale": "en", + "alternate_locales": [] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "domains/create", + "x-shopify-triggered-at": "2024-08-07T22:53:11.795486444Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "domains/destroy", + "name": "domains/destroy", + "body": { + "id": 690933842, + "host": "jsmith.myshopify.com", + "ssl_enabled": true, + "localization": { + "country": null, + "default_locale": "en", + "alternate_locales": [] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "domains/destroy", + "x-shopify-triggered-at": "2024-08-07T22:53:16.966800935Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "domains/update", + "name": "domains/update", + "body": { + "id": 690933842, + "host": "jsmith.myshopify.com", + "ssl_enabled": true, + "localization": { + "country": null, + "default_locale": "en", + "alternate_locales": [] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "domains/update", + "x-shopify-triggered-at": "2024-08-07T22:53:22.021064442Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "draft_orders/create", + "name": "draft_orders/create", + "body": { + "id": 890612572568261600, + "note": null, + "email": "jon@doe.ca", + "taxes_included": false, + "currency": "USD", + "invoice_sent_at": null, + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "tax_exempt": false, + "completed_at": null, + "name": "#D128", + "status": "open", + "line_items": [ + { + "id": 1353766, + "variant_id": 49148385, + "product_id": 632910392, + "title": "IPod Nano - 8GB", + "variant_title": "Red", + "sku": "IPOD2008RED", + "vendor": "Apple", + "quantity": 4, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "fulfillment_service": "manual", + "grams": 567, + "tax_lines": [], + "applied_discount": null, + "name": "IPod Nano - 8GB - Red", + "properties": [], + "custom": false, + "price": "199.00", + "admin_graphql_api_id": "gid://shopify/DraftOrderLineItem/1353766" + }, + { + "id": 2158714, + "variant_id": 457924702, + "product_id": 632910392, + "title": "IPod Nano - 8GB", + "variant_title": "Black", + "sku": "IPOD2008BLACK", + "vendor": "Apple", + "quantity": 7, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "fulfillment_service": "manual", + "grams": 567, + "tax_lines": [], + "applied_discount": null, + "name": "IPod Nano - 8GB - Black", + "properties": [], + "custom": false, + "price": "199.00", + "admin_graphql_api_id": "gid://shopify/DraftOrderLineItem/2158714" + }, + { + "id": 1453233, + "variant_id": 808950810, + "product_id": 632910392, + "title": "IPod Nano - 8GB", + "variant_title": "Pink", + "sku": "IPOD2008PINK", + "vendor": "Apple", + "quantity": 9, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "fulfillment_service": "manual", + "grams": 567, + "tax_lines": [], + "applied_discount": null, + "name": "IPod Nano - 8GB - Pink", + "properties": [], + "custom": false, + "price": "199.00", + "admin_graphql_api_id": "gid://shopify/DraftOrderLineItem/1453233" + } + ], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40150", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "billing_address": { + "first_name": "Bob", + "address1": "123 Billing Street", + "phone": "555-555-BILL", + "city": "Billtown", + "zip": "K2P0B0", + "province": "Kentucky", + "country": "United States", + "last_name": "Biller", + "address2": null, + "company": "My Company", + "latitude": null, + "longitude": null, + "name": "Bob Biller", + "country_code": "US", + "province_code": "KY" + }, + "invoice_url": "https://jsmith.myshopify.com/548380009/invoices/abcd1234abcd1234abcd1234abcd1234", + "applied_discount": { + "description": "ABC 123", + "value": "3.0", + "title": "ABC 123", + "amount": "2.00", + "value_type": "percentage" + }, + "order_id": null, + "shipping_line": { + "title": "ABC 123", + "custom": true, + "handle": null, + "price": "5.00" + }, + "tax_lines": [ + { + "rate": 0.017665192468618723, + "title": "State tax", + "price": "5.00" + }, + { + "rate": 0.017665192468618723, + "title": "State tax", + "price": "3.00" + }, + { + "rate": 0.017665192468618723, + "title": "State tax", + "price": "3.00" + } + ], + "tags": "", + "note_attributes": [], + "total_price": "407.00", + "subtotal_price": "412.00", + "total_tax": "449.00", + "payment_terms": { + "id": 706405506930370000, + "payment_terms_name": "Net 7", + "payment_terms_type": "net", + "due_in_days": 7, + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "payment_schedules": [ + { + "id": 606405506930370000, + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "payment_terms_id": 706405506930370000, + "reference_id": null, + "reference_type": null, + "issued_at": "2021-01-01T00:00:00-05:00", + "due_at": "2021-01-02T00:00:00-05:00", + "completed_at": "2021-01-02T00:00:00-05:00", + "amount": "10.00", + "currency": "USD" + } + ], + "can_pay_early": true + }, + "admin_graphql_api_id": "gid://shopify/DraftOrder/890612572568261625", + "customer": { + "id": 706405506930370000, + "email": "john@doe.ca", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "tax_exemptions": [], + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084", + "default_address": { + "id": null, + "customer_id": 706405506930370000, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "draft_orders/create", + "x-shopify-triggered-at": "2024-08-07T22:53:27.296234376Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "draft_orders/delete", + "name": "draft_orders/delete", + "body": { + "id": 890612572568261600 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "draft_orders/delete", + "x-shopify-triggered-at": "2024-08-07T22:53:32.503281901Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "draft_orders/update", + "name": "draft_orders/update", + "body": { + "id": 890612572568261600, + "note": null, + "email": "jon@doe.ca", + "taxes_included": false, + "currency": "USD", + "invoice_sent_at": null, + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "tax_exempt": false, + "completed_at": null, + "name": "#D123", + "status": "open", + "line_items": [ + { + "id": 1596065, + "variant_id": 49148385, + "product_id": 632910392, + "title": "IPod Nano - 8GB", + "variant_title": "Red", + "sku": "IPOD2008RED", + "vendor": "Apple", + "quantity": 1, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "fulfillment_service": "manual", + "grams": 567, + "tax_lines": [], + "applied_discount": null, + "name": "IPod Nano - 8GB - Red", + "properties": [], + "custom": false, + "price": "199.00", + "admin_graphql_api_id": "gid://shopify/DraftOrderLineItem/1596065" + }, + { + "id": 1491517, + "variant_id": 457924702, + "product_id": 632910392, + "title": "IPod Nano - 8GB", + "variant_title": "Black", + "sku": "IPOD2008BLACK", + "vendor": "Apple", + "quantity": 3, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "fulfillment_service": "manual", + "grams": 567, + "tax_lines": [], + "applied_discount": null, + "name": "IPod Nano - 8GB - Black", + "properties": [], + "custom": false, + "price": "199.00", + "admin_graphql_api_id": "gid://shopify/DraftOrderLineItem/1491517" + }, + { + "id": 1101873, + "variant_id": 808950810, + "product_id": 632910392, + "title": "IPod Nano - 8GB", + "variant_title": "Pink", + "sku": "IPOD2008PINK", + "vendor": "Apple", + "quantity": 3, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "fulfillment_service": "manual", + "grams": 567, + "tax_lines": [], + "applied_discount": null, + "name": "IPod Nano - 8GB - Pink", + "properties": [], + "custom": false, + "price": "199.00", + "admin_graphql_api_id": "gid://shopify/DraftOrderLineItem/1101873" + } + ], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40150", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "billing_address": { + "first_name": "Bob", + "address1": "123 Billing Street", + "phone": "555-555-BILL", + "city": "Billtown", + "zip": "K2P0B0", + "province": "Kentucky", + "country": "United States", + "last_name": "Biller", + "address2": null, + "company": "My Company", + "latitude": null, + "longitude": null, + "name": "Bob Biller", + "country_code": "US", + "province_code": "KY" + }, + "invoice_url": "https://jsmith.myshopify.com/548380009/invoices/abcd1234abcd1234abcd1234abcd1234", + "applied_discount": { + "description": "ABC 123", + "value": "6.0", + "title": "ABC 123", + "amount": "1.00", + "value_type": "percentage" + }, + "order_id": null, + "shipping_line": { + "title": "ABC 123", + "custom": true, + "handle": null, + "price": "1.00" + }, + "tax_lines": [ + { + "rate": 0.045697444018947916, + "title": "State tax", + "price": "3.00" + }, + { + "rate": 0.045697444018947916, + "title": "State tax", + "price": "3.00" + }, + { + "rate": 0.045697444018947916, + "title": "State tax", + "price": "2.00" + } + ], + "tags": "", + "note_attributes": [], + "total_price": "445.00", + "subtotal_price": "408.00", + "total_tax": "406.00", + "payment_terms": { + "id": 706405506930370000, + "payment_terms_name": "Net 7", + "payment_terms_type": "net", + "due_in_days": 7, + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "payment_schedules": [ + { + "id": 606405506930370000, + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "payment_terms_id": 706405506930370000, + "reference_id": null, + "reference_type": null, + "issued_at": "2021-01-01T00:00:00-05:00", + "due_at": "2021-01-02T00:00:00-05:00", + "completed_at": "2021-01-02T00:00:00-05:00", + "amount": "10.00", + "currency": "USD" + } + ], + "can_pay_early": true + }, + "admin_graphql_api_id": "gid://shopify/DraftOrder/890612572568261625", + "customer": { + "id": 706405506930370000, + "email": "john@doe.ca", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "orders_count": 0, + "state": "disabled", + "total_spent": "0.00", + "last_order_id": null, + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "tags": "", + "last_order_name": null, + "currency": "USD", + "phone": null, + "tax_exemptions": [], + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "admin_graphql_api_id": "gid://shopify/Customer/706405506930370084", + "default_address": { + "id": null, + "customer_id": 706405506930370000, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "draft_orders/update", + "x-shopify-triggered-at": "2024-08-07T22:53:37.729407140Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_events/create", + "name": "fulfillment_events/create", + "body": { + "id": 1234567, + "fulfillment_id": 123456, + "status": "in_transit", + "message": "Item is now in transit", + "happened_at": "2021-12-31T19:00:00-05:00", + "city": null, + "province": null, + "country": "CA", + "zip": null, + "address1": null, + "latitude": null, + "longitude": null, + "shop_id": 548380009, + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "estimated_delivery_at": null, + "order_id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/1234567" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_events/create", + "x-shopify-triggered-at": "2024-08-07T22:54:12.513618517Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_events/delete", + "name": "fulfillment_events/delete", + "body": { + "id": 1234567, + "fulfillment_id": 123456, + "status": "in_transit", + "message": "Item is now in transit", + "happened_at": "2021-12-31T19:00:00-05:00", + "city": null, + "province": null, + "country": "CA", + "zip": null, + "address1": null, + "latitude": null, + "longitude": null, + "shop_id": 548380009, + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "estimated_delivery_at": null, + "order_id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/1234567" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_events/delete", + "x-shopify-triggered-at": "2024-08-07T22:54:17.632607837Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/cancellation_request_accepted", + "name": "fulfillment_orders/cancellation_request_accepted", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "closed" + }, + "message": "Order has not been shipped yet." + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/cancellation_request_accepted", + "x-shopify-triggered-at": "2024-08-07T22:54:22.947401466Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/cancellation_request_rejected", + "name": "fulfillment_orders/cancellation_request_rejected", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "in_progress", + "request_status": "cancellation_rejected" + }, + "message": "Order has already been shipped." + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/cancellation_request_rejected", + "x-shopify-triggered-at": "2024-08-07T22:54:28.194322517Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/cancellation_request_submitted", + "name": "fulfillment_orders/cancellation_request_submitted", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "in_progress", + "request_status": "cancellation_request" + }, + "fulfillment_order_merchant_request": { + "id": "gid://shopify/FulfillmentOrderMerchantRequest/1", + "message": "Customer cancelled their order" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/cancellation_request_submitted", + "x-shopify-triggered-at": "2024-08-07T22:54:33.291162653Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/cancelled", + "name": "fulfillment_orders/cancelled", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "cancelled" + }, + "replacement_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/2", + "status": "open" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/cancelled", + "x-shopify-triggered-at": "2024-08-07T22:54:38.378505209Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/fulfillment_request_accepted", + "name": "fulfillment_orders/fulfillment_request_accepted", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "in_progress", + "request_status": "accepted" + }, + "message": "We will ship the item tomorrow." + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/fulfillment_request_accepted", + "x-shopify-triggered-at": "2024-08-07T22:54:43.513563899Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/fulfillment_request_rejected", + "name": "fulfillment_orders/fulfillment_request_rejected", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open", + "request_status": "rejected" + }, + "message": "Can't fulfill due to no inventory on product." + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/fulfillment_request_rejected", + "x-shopify-triggered-at": "2024-08-07T22:54:48.679476577Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/fulfillment_request_submitted", + "name": "fulfillment_orders/fulfillment_request_submitted", + "body": { + "original_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open", + "request_status": "unsubmitted" + }, + "submitted_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open", + "request_status": "unsubmitted" + }, + "fulfillment_order_merchant_request": { + "id": "gid://shopify/FulfillmentOrderMerchantRequest/1", + "message": "Fragile" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/fulfillment_request_submitted", + "x-shopify-triggered-at": "2024-08-07T22:54:53.730711129Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/fulfillment_service_failed_to_complete", + "name": "fulfillment_orders/fulfillment_service_failed_to_complete", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "closed" + }, + "message": "We broke the last item." + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/fulfillment_service_failed_to_complete", + "x-shopify-triggered-at": "2024-08-07T22:54:59.251945239Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/hold_released", + "name": "fulfillment_orders/hold_released", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/hold_released", + "x-shopify-triggered-at": "2024-08-07T22:55:04.432527192Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/line_items_prepared_for_local_delivery", + "name": "fulfillment_orders/line_items_prepared_for_local_delivery", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open", + "preparable": true, + "delivery_method": { + "method_type": "local" + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/line_items_prepared_for_local_delivery", + "x-shopify-triggered-at": "2024-08-07T22:55:09.748104157Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/line_items_prepared_for_pickup", + "name": "fulfillment_orders/line_items_prepared_for_pickup", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open", + "preparable": true, + "delivery_method": { + "method_type": "pickup" + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/line_items_prepared_for_pickup", + "x-shopify-triggered-at": "2024-08-07T22:55:15.149205926Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/merged", + "name": "fulfillment_orders/merged", + "body": { + "merge_intents": [ + { + "fulfillment_order_id": 1, + "fulfillment_order_line_items": [ + { + "id": 1, + "quantity": 1 + } + ] + }, + { + "fulfillment_order_id": 2, + "fulfillment_order_line_items": [ + { + "id": 2, + "quantity": 1 + } + ] + } + ], + "fulfillment_order_merges": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open" + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/merged", + "x-shopify-triggered-at": "2024-08-07T22:55:20.701324075Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/moved", + "name": "fulfillment_orders/moved", + "body": { + "original_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "closed", + "assigned_location_id": "gid://shopify/Location/0" + }, + "moved_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/2", + "status": "open", + "assigned_location_id": "gid://shopify/Location/1" + }, + "destination_location_id": "gid://shopify/Location/1", + "fulfillment_order_line_items_requested": [ + { + "id": "gid://shopify/FulfillmentOrderLineItem/1", + "quantity": 1 + } + ], + "source_location": { + "id": "gid://shopify/Location/0" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/moved", + "x-shopify-triggered-at": "2024-08-07T22:55:25.839190078Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/order_routing_complete", + "name": "fulfillment_orders/order_routing_complete", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/order_routing_complete", + "x-shopify-triggered-at": "2024-08-07T22:55:31.300557451Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/placed_on_hold", + "name": "fulfillment_orders/placed_on_hold", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "on_hold", + "fulfillment_holds": [ + { + "id": "gid://shopify/FulfillmentHold/1", + "held_by_requesting_app": false, + "reason": "other", + "reason_notes": "example" + } + ] + }, + "remaining_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/2", + "status": "open" + }, + "held_fulfillment_order_line_items": [ + { + "id": "gid://shopify/FulfillmentOrderLineItem/3", + "quantity": 4 + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/placed_on_hold", + "x-shopify-triggered-at": "2024-08-07T22:55:36.572885269Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/rescheduled", + "name": "fulfillment_orders/rescheduled", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "scheduled", + "fulfill_at": "2021-12-31T19:00:00-05:00" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/rescheduled", + "x-shopify-triggered-at": "2024-08-07T22:55:46.021237736Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/scheduled_fulfillment_order_ready", + "name": "fulfillment_orders/scheduled_fulfillment_order_ready", + "body": { + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/scheduled_fulfillment_order_ready", + "x-shopify-triggered-at": "2024-08-07T22:55:51.240125874Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillment_orders/split", + "name": "fulfillment_orders/split", + "body": { + "split_line_items": [ + { + "id": "gid://shopify/FulfillmentOrderLineItem/1", + "quantity": 1 + } + ], + "fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/1", + "status": "open" + }, + "remaining_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/2", + "status": "open" + }, + "replacement_fulfillment_order": { + "id": "gid://shopify/FulfillmentOrder/3", + "status": "open" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillment_orders/split", + "x-shopify-triggered-at": "2024-08-07T22:55:56.429089585Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillments/create", + "name": "fulfillments/create", + "body": { + "id": 123456, + "order_id": 820982911946154500, + "status": "pending", + "created_at": "2021-12-31T19:00:00-05:00", + "service": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "tracking_company": "UPS", + "shipment_status": null, + "location_id": null, + "origin_address": null, + "email": "jon@example.com", + "destination": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "line_items": [ + { + "id": 866550311766439000, + "variant_id": 808950810, + "title": "IPod Nano - 8GB", + "quantity": 1, + "sku": "IPOD2008PINK", + "variant_title": null, + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8GB", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 567, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "tax_lines": [] + }, + { + "id": 141249953214522980, + "variant_id": 808950810, + "title": "IPod Nano - 8GB", + "quantity": 1, + "sku": "IPOD2008PINK", + "variant_title": null, + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8GB", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 567, + "price": "199.00", + "total_discount": "5.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "5.00", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + } + }, + { + "amount": "5.00", + "discount_application_index": 2, + "amount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "tax_lines": [] + } + ], + "tracking_number": "1z827wk74630", + "tracking_numbers": [ + "1z827wk74630" + ], + "tracking_url": "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630", + "tracking_urls": [ + "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630" + ], + "receipt": {}, + "name": "#9999.1", + "admin_graphql_api_id": "gid://shopify/Fulfillment/123456" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillments/create", + "x-shopify-triggered-at": "2024-08-07T22:54:01.001600265Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "fulfillments/update", + "name": "fulfillments/update", + "body": { + "id": 123456, + "order_id": 820982911946154500, + "status": "pending", + "created_at": "2021-12-31T19:00:00-05:00", + "service": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "tracking_company": "UPS", + "shipment_status": null, + "location_id": null, + "origin_address": null, + "email": "jon@example.com", + "destination": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "line_items": [ + { + "id": 866550311766439000, + "variant_id": 808950810, + "title": "IPod Nano - 8GB", + "quantity": 1, + "sku": "IPOD2008PINK", + "variant_title": null, + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8GB", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 567, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "tax_lines": [] + }, + { + "id": 141249953214522980, + "variant_id": 808950810, + "title": "IPod Nano - 8GB", + "quantity": 1, + "sku": "IPOD2008PINK", + "variant_title": null, + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8GB", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 567, + "price": "199.00", + "total_discount": "5.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "5.00", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + } + }, + { + "amount": "5.00", + "discount_application_index": 2, + "amount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "tax_lines": [] + } + ], + "tracking_number": "1z827wk74630", + "tracking_numbers": [ + "1z827wk74630" + ], + "tracking_url": "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630", + "tracking_urls": [ + "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630" + ], + "receipt": {}, + "name": "#9999.1", + "admin_graphql_api_id": "gid://shopify/Fulfillment/123456" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "fulfillments/update", + "x-shopify-triggered-at": "2024-08-07T22:54:06.433972696Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "inventory_items/create", + "name": "inventory_items/create", + "body": { + "id": 271878346596884000, + "sku": "example-sku", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "requires_shipping": true, + "cost": null, + "country_code_of_origin": null, + "province_code_of_origin": null, + "harmonized_system_code": null, + "tracked": true, + "country_harmonized_system_codes": [], + "admin_graphql_api_id": "gid://shopify/InventoryItem/271878346596884015" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "inventory_items/create", + "x-shopify-triggered-at": "2024-08-07T22:56:01.805811346Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "inventory_items/delete", + "name": "inventory_items/delete", + "body": { + "id": 271878346596884000, + "country_code_of_origin": null, + "province_code_of_origin": null, + "harmonized_system_code": null, + "country_harmonized_system_codes": [], + "admin_graphql_api_id": "gid://shopify/InventoryItem/271878346596884015" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "inventory_items/delete", + "x-shopify-triggered-at": "2024-08-07T22:56:07.575581829Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "inventory_items/update", + "name": "inventory_items/update", + "body": { + "id": 271878346596884000, + "sku": "example-sku", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "requires_shipping": true, + "cost": null, + "country_code_of_origin": null, + "province_code_of_origin": null, + "harmonized_system_code": null, + "tracked": true, + "country_harmonized_system_codes": [], + "admin_graphql_api_id": "gid://shopify/InventoryItem/271878346596884015" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "inventory_items/update", + "x-shopify-triggered-at": "2024-08-07T22:56:13.034845666Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "inventory_levels/connect", + "name": "inventory_levels/connect", + "body": { + "inventory_item_id": 271878346596884000, + "location_id": 24826418, + "available": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/InventoryLevel/24826418?inventory_item_id=271878346596884015" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "inventory_levels/connect", + "x-shopify-triggered-at": "2024-08-07T22:56:18.293174977Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "inventory_levels/disconnect", + "name": "inventory_levels/disconnect", + "body": { + "inventory_item_id": 271878346596884000, + "location_id": 24826418 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "inventory_levels/disconnect", + "x-shopify-triggered-at": "2024-08-07T22:56:23.647670650Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "inventory_levels/update", + "name": "inventory_levels/update", + "body": { + "inventory_item_id": 271878346596884000, + "location_id": 24826418, + "available": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "admin_graphql_api_id": "gid://shopify/InventoryLevel/24826418?inventory_item_id=271878346596884015" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "inventory_levels/update", + "x-shopify-triggered-at": "2024-08-07T22:56:28.834278316Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locales/create", + "name": "locales/create", + "body": { + "locale": "fr-CA", + "published": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locales/create", + "x-shopify-triggered-at": "2024-08-07T22:56:33.940054652Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locales/update", + "name": "locales/update", + "body": { + "locale": "fr-CA", + "published": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locales/update", + "x-shopify-triggered-at": "2024-08-07T22:56:43.082934480Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locations/activate", + "name": "locations/activate", + "body": { + "id": 866550311766439000, + "name": "Example Shop", + "address1": "34 Example Street", + "address2": "Next to example", + "city": "ottawa", + "zip": "k1n5t5", + "province": "ontario", + "country": "CA", + "phone": "555-555-5555", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "country_code": "CA", + "country_name": "Canada", + "province_code": "ON", + "legacy": false, + "active": true, + "admin_graphql_api_id": "gid://shopify/Location/866550311766439020" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locations/activate", + "x-shopify-triggered-at": "2024-08-07T22:56:48.637120681Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locations/create", + "name": "locations/create", + "body": { + "id": 866550311766439000, + "name": "Example Shop", + "address1": "34 Example Street", + "address2": "Next to example", + "city": "ottawa", + "zip": "k1n5t5", + "province": "ontario", + "country": "CA", + "phone": "555-555-5555", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "country_code": "CA", + "country_name": "Canada", + "province_code": "ON", + "legacy": false, + "active": true, + "admin_graphql_api_id": "gid://shopify/Location/866550311766439020" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locations/create", + "x-shopify-triggered-at": "2024-08-07T22:56:54.033291393Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locations/deactivate", + "name": "locations/deactivate", + "body": { + "id": 866550311766439000, + "name": "Example Shop", + "address1": "34 Example Street", + "address2": "Next to example", + "city": "ottawa", + "zip": "k1n5t5", + "province": "ontario", + "country": "CA", + "phone": "555-555-5555", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "country_code": "CA", + "country_name": "Canada", + "province_code": "ON", + "legacy": false, + "active": true, + "admin_graphql_api_id": "gid://shopify/Location/866550311766439020" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locations/deactivate", + "x-shopify-triggered-at": "2024-08-07T22:56:59.219600432Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locations/delete", + "name": "locations/delete", + "body": { + "id": 866550311766439000 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locations/delete", + "x-shopify-triggered-at": "2024-08-07T22:57:04.832897130Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "locations/update", + "name": "locations/update", + "body": { + "id": 866550311766439000, + "name": "Example Shop", + "address1": "34 Example Street", + "address2": "Next to example", + "city": "ottawa", + "zip": "k1n5t5", + "province": "ontario", + "country": "CA", + "phone": "555-555-5555", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "country_code": "CA", + "country_name": "Canada", + "province_code": "ON", + "legacy": false, + "active": true, + "admin_graphql_api_id": "gid://shopify/Location/866550311766439020" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "locations/update", + "x-shopify-triggered-at": "2024-08-07T22:57:11.479946918Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "markets/create", + "name": "markets/create", + "body": { + "id": 188558248, + "name": "United States", + "enabled": true, + "regions": [ + { + "country_code": "US" + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "markets/create", + "x-shopify-triggered-at": "2024-08-07T22:57:20.292701775Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "markets/delete", + "name": "markets/delete", + "body": { + "id": 188558248 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "markets/delete", + "x-shopify-triggered-at": "2024-08-07T22:57:25.470119112Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "markets/update", + "name": "markets/update", + "body": { + "id": 188558248, + "name": "United States", + "enabled": true, + "regions": [ + { + "country_code": "US" + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "markets/update", + "x-shopify-triggered-at": "2024-08-07T22:57:30.583400965Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "metaobjects/create", + "name": "metaobjects/create", + "body": { + "type": "example-type", + "handle": "example-metaobject", + "created_at": "1975-10-31T00:55:12-05:00", + "updated_at": "1985-07-13T01:55:12-04:00", + "display_name": "Example Metaobject", + "id": "gid://shopify/Metaobject/123", + "definition_id": "gid://shopify/MetaobjectDefinition/222", + "fields": { + "example-key": "example-value" + }, + "created_by_staff_id": "gid://shopify/StaffMember/444", + "created_by_app_id": "gid://shopify/ApiClient/333", + "capabilities": { + "publishable": { + "status": "draft" + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "metaobjects/create", + "x-shopify-triggered-at": "2024-08-07T22:57:36.305844942Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "metaobjects/delete", + "name": "metaobjects/delete", + "body": { + "id": "gid://shopify/Metaobject/123", + "type": "example-type", + "handle": "example-metaobject", + "created_by_app_id": "gid://shopify/ApiClient/333" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "metaobjects/delete", + "x-shopify-triggered-at": "2024-08-07T22:57:41.651693761Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "metaobjects/update", + "name": "metaobjects/update", + "body": { + "type": "example-type", + "handle": "example-metaobject", + "created_at": "1975-10-31T00:55:12-05:00", + "updated_at": "1985-07-13T01:55:12-04:00", + "display_name": "Example Metaobject", + "id": "gid://shopify/Metaobject/123", + "definition_id": "gid://shopify/MetaobjectDefinition/222", + "fields": { + "example-key": "example-value" + }, + "created_by_staff_id": "gid://shopify/StaffMember/444", + "created_by_app_id": "gid://shopify/ApiClient/333", + "capabilities": { + "publishable": { + "status": "draft" + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "metaobjects/update", + "x-shopify-triggered-at": "2024-08-07T22:57:47.264735263Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "order_transactions/create", + "name": "order_transactions/create", + "body": { + "id": 120560818172775260, + "order_id": 820982911946154500, + "kind": "authorization", + "gateway": "visa", + "status": "success", + "message": null, + "created_at": "2021-12-31T19:00:00-05:00", + "test": false, + "authorization": "1001", + "location_id": null, + "user_id": null, + "parent_id": null, + "processed_at": null, + "device_id": null, + "error_code": null, + "source_name": "web", + "payment_details": { + "credit_card_bin": null, + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 1234", + "credit_card_company": "Visa", + "buyer_action_info": null, + "credit_card_name": null, + "credit_card_wallet": null, + "credit_card_expiration_month": null, + "credit_card_expiration_year": null, + "payment_method_name": "visa" + }, + "receipt": {}, + "amount": "403.00", + "currency": "USD", + "payment_id": "#9999.1", + "total_unsettled_set": { + "presentment_money": { + "amount": "403.0", + "currency": "USD" + }, + "shop_money": { + "amount": "403.0", + "currency": "USD" + } + }, + "manual_payment_gateway": true, + "admin_graphql_api_id": "gid://shopify/OrderTransaction/120560818172775265" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "order_transactions/create", + "x-shopify-triggered-at": "2024-08-07T22:58:48.853958698Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/cancelled", + "name": "orders/cancelled", + "body": { + "id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/Order/820982911946154508", + "app_id": null, + "browser_ip": null, + "buyer_accepts_marketing": true, + "cancel_reason": "customer", + "cancelled_at": "2021-12-31T19:00:00-05:00", + "cart_token": null, + "checkout_id": null, + "checkout_token": null, + "client_details": null, + "closed_at": null, + "confirmation_number": null, + "confirmed": false, + "contact_email": "jon@example.com", + "created_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "current_subtotal_price": "398.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_additional_fees_set": null, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "current_total_duties_set": null, + "current_total_price": "398.00", + "current_total_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "customer_locale": "en", + "device_id": null, + "discount_codes": [], + "email": "jon@example.com", + "estimated_taxes": false, + "financial_status": "voided", + "fulfillment_status": "pending", + "landing_site": null, + "landing_site_ref": null, + "location_id": null, + "merchant_of_record_app_id": null, + "name": "#9999", + "note": null, + "note_attributes": [], + "number": 234, + "order_number": 1234, + "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + "original_total_additional_fees_set": null, + "original_total_duties_set": null, + "payment_gateway_names": [ + "visa", + "bogus" + ], + "phone": null, + "po_number": null, + "presentment_currency": "USD", + "processed_at": "2021-12-31T19:00:00-05:00", + "reference": null, + "referring_site": null, + "source_identifier": null, + "source_name": "web", + "source_url": null, + "subtotal_price": "388.00", + "subtotal_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "tags": "tag1, tag2", + "tax_exempt": false, + "tax_lines": [], + "taxes_included": false, + "test": true, + "token": "123456abcd", + "total_discounts": "20.00", + "total_discounts_set": { + "shop_money": { + "amount": "20.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "20.00", + "currency_code": "USD" + } + }, + "total_line_items_price": "398.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "total_outstanding": "398.00", + "total_price": "388.00", + "total_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "total_shipping_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2021-12-31T19:00:00-05:00", + "user_id": null, + "billing_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 115310627314723950, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "state": "disabled", + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "tags": "", + "currency": "USD", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954", + "default_address": { + "id": 715243470612851200, + "customer_id": 115310627314723950, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 866550311766439000, + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "attributed_staffs": [ + { + "id": "gid://shopify/StaffMember/902541635", + "quantity": 1 + } + ], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + }, + { + "id": 141249953214522980, + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "attributed_staffs": [], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "shipping_lines": [ + { + "id": 271878346596884000, + "carrier_identifier": null, + "code": null, + "discounted_price": "10.00", + "discounted_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "is_removed": false, + "phone": null, + "price": "10.00", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Generic Shipping", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/cancelled", + "x-shopify-triggered-at": "2024-08-07T22:57:52.338800668Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/create", + "name": "orders/create", + "body": { + "id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/Order/820982911946154508", + "app_id": null, + "browser_ip": null, + "buyer_accepts_marketing": true, + "cancel_reason": "customer", + "cancelled_at": "2021-12-31T19:00:00-05:00", + "cart_token": null, + "checkout_id": null, + "checkout_token": null, + "client_details": null, + "closed_at": null, + "confirmation_number": null, + "confirmed": false, + "contact_email": "jon@example.com", + "created_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "current_subtotal_price": "398.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_additional_fees_set": null, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "current_total_duties_set": null, + "current_total_price": "398.00", + "current_total_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "customer_locale": "en", + "device_id": null, + "discount_codes": [], + "email": "jon@example.com", + "estimated_taxes": false, + "financial_status": "voided", + "fulfillment_status": "pending", + "landing_site": null, + "landing_site_ref": null, + "location_id": null, + "merchant_of_record_app_id": null, + "name": "#9999", + "note": null, + "note_attributes": [], + "number": 234, + "order_number": 1234, + "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + "original_total_additional_fees_set": null, + "original_total_duties_set": null, + "payment_gateway_names": [ + "visa", + "bogus" + ], + "phone": null, + "po_number": null, + "presentment_currency": "USD", + "processed_at": "2021-12-31T19:00:00-05:00", + "reference": null, + "referring_site": null, + "source_identifier": null, + "source_name": "web", + "source_url": null, + "subtotal_price": "388.00", + "subtotal_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "tags": "tag1, tag2", + "tax_exempt": false, + "tax_lines": [], + "taxes_included": false, + "test": true, + "token": "123456abcd", + "total_discounts": "20.00", + "total_discounts_set": { + "shop_money": { + "amount": "20.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "20.00", + "currency_code": "USD" + } + }, + "total_line_items_price": "398.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "total_outstanding": "398.00", + "total_price": "388.00", + "total_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "total_shipping_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2021-12-31T19:00:00-05:00", + "user_id": null, + "billing_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 115310627314723950, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "state": "disabled", + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "tags": "", + "currency": "USD", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954", + "default_address": { + "id": 715243470612851200, + "customer_id": 115310627314723950, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 866550311766439000, + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "attributed_staffs": [ + { + "id": "gid://shopify/StaffMember/902541635", + "quantity": 1 + } + ], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + }, + { + "id": 141249953214522980, + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "attributed_staffs": [], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "shipping_lines": [ + { + "id": 271878346596884000, + "carrier_identifier": null, + "code": null, + "discounted_price": "10.00", + "discounted_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "is_removed": false, + "phone": null, + "price": "10.00", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Generic Shipping", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/create", + "x-shopify-triggered-at": "2024-08-07T22:57:57.290670248Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/delete", + "name": "orders/delete", + "body": { + "id": 820982911946154500 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/delete", + "x-shopify-triggered-at": "2024-08-07T22:58:02.728860151Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/edited", + "name": "orders/edited", + "body": { + "order_edit": { + "id": 78912328793123780, + "app_id": null, + "created_at": "2021-12-31T19:00:00-05:00", + "committed_at": "2021-12-31T19:00:00-05:00", + "notify_customer": false, + "order_id": 820982911946154500, + "staff_note": "", + "user_id": null, + "line_items": { + "additions": [ + { + "id": 78643924236718240, + "delta": 1 + } + ], + "removals": [ + { + "id": 866550311766439000, + "delta": 1 + } + ] + }, + "discounts": { + "line_item": { + "additions": [], + "removals": [] + } + }, + "shipping_lines": { + "additions": [], + "removals": [] + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/edited", + "x-shopify-triggered-at": "2024-08-07T22:58:08.064630177Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/fulfilled", + "name": "orders/fulfilled", + "body": { + "id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/Order/820982911946154508", + "app_id": null, + "browser_ip": null, + "buyer_accepts_marketing": true, + "cancel_reason": "customer", + "cancelled_at": "2021-12-31T19:00:00-05:00", + "cart_token": null, + "checkout_id": null, + "checkout_token": null, + "client_details": null, + "closed_at": null, + "confirmation_number": null, + "confirmed": false, + "contact_email": "jon@example.com", + "created_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "current_subtotal_price": "398.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_additional_fees_set": null, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "current_total_duties_set": null, + "current_total_price": "398.00", + "current_total_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "customer_locale": "en", + "device_id": null, + "discount_codes": [], + "email": "jon@example.com", + "estimated_taxes": false, + "financial_status": "voided", + "fulfillment_status": "pending", + "landing_site": null, + "landing_site_ref": null, + "location_id": null, + "merchant_of_record_app_id": null, + "name": "#9999", + "note": null, + "note_attributes": [], + "number": 234, + "order_number": 1234, + "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + "original_total_additional_fees_set": null, + "original_total_duties_set": null, + "payment_gateway_names": [ + "visa", + "bogus" + ], + "phone": null, + "po_number": null, + "presentment_currency": "USD", + "processed_at": "2021-12-31T19:00:00-05:00", + "reference": null, + "referring_site": null, + "source_identifier": null, + "source_name": "web", + "source_url": null, + "subtotal_price": "388.00", + "subtotal_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "tags": "tag1, tag2", + "tax_exempt": false, + "tax_lines": [], + "taxes_included": false, + "test": true, + "token": "123456abcd", + "total_discounts": "20.00", + "total_discounts_set": { + "shop_money": { + "amount": "20.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "20.00", + "currency_code": "USD" + } + }, + "total_line_items_price": "398.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "total_outstanding": "398.00", + "total_price": "388.00", + "total_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "total_shipping_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2021-12-31T19:00:00-05:00", + "user_id": null, + "billing_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 115310627314723950, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "state": "disabled", + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "tags": "", + "currency": "USD", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954", + "default_address": { + "id": 715243470612851200, + "customer_id": 115310627314723950, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 866550311766439000, + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "attributed_staffs": [ + { + "id": "gid://shopify/StaffMember/902541635", + "quantity": 1 + } + ], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + }, + { + "id": 141249953214522980, + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "attributed_staffs": [], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "shipping_lines": [ + { + "id": 271878346596884000, + "carrier_identifier": null, + "code": null, + "discounted_price": "10.00", + "discounted_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "is_removed": false, + "phone": null, + "price": "10.00", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Generic Shipping", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/fulfilled", + "x-shopify-triggered-at": "2024-08-07T22:58:13.384877484Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/paid", + "name": "orders/paid", + "body": { + "id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/Order/820982911946154508", + "app_id": null, + "browser_ip": null, + "buyer_accepts_marketing": true, + "cancel_reason": "customer", + "cancelled_at": "2021-12-31T19:00:00-05:00", + "cart_token": null, + "checkout_id": null, + "checkout_token": null, + "client_details": null, + "closed_at": null, + "confirmation_number": null, + "confirmed": false, + "contact_email": "jon@example.com", + "created_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "current_subtotal_price": "398.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_additional_fees_set": null, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "current_total_duties_set": null, + "current_total_price": "398.00", + "current_total_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "customer_locale": "en", + "device_id": null, + "discount_codes": [], + "email": "jon@example.com", + "estimated_taxes": false, + "financial_status": "voided", + "fulfillment_status": "pending", + "landing_site": null, + "landing_site_ref": null, + "location_id": null, + "merchant_of_record_app_id": null, + "name": "#9999", + "note": null, + "note_attributes": [], + "number": 234, + "order_number": 1234, + "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + "original_total_additional_fees_set": null, + "original_total_duties_set": null, + "payment_gateway_names": [ + "visa", + "bogus" + ], + "phone": null, + "po_number": null, + "presentment_currency": "USD", + "processed_at": "2021-12-31T19:00:00-05:00", + "reference": null, + "referring_site": null, + "source_identifier": null, + "source_name": "web", + "source_url": null, + "subtotal_price": "388.00", + "subtotal_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "tags": "tag1, tag2", + "tax_exempt": false, + "tax_lines": [], + "taxes_included": false, + "test": true, + "token": "123456abcd", + "total_discounts": "20.00", + "total_discounts_set": { + "shop_money": { + "amount": "20.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "20.00", + "currency_code": "USD" + } + }, + "total_line_items_price": "398.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "total_outstanding": "398.00", + "total_price": "388.00", + "total_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "total_shipping_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2021-12-31T19:00:00-05:00", + "user_id": null, + "billing_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 115310627314723950, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "state": "disabled", + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "tags": "", + "currency": "USD", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954", + "default_address": { + "id": 715243470612851200, + "customer_id": 115310627314723950, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 866550311766439000, + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "attributed_staffs": [ + { + "id": "gid://shopify/StaffMember/902541635", + "quantity": 1 + } + ], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + }, + { + "id": 141249953214522980, + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "attributed_staffs": [], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "shipping_lines": [ + { + "id": 271878346596884000, + "carrier_identifier": null, + "code": null, + "discounted_price": "10.00", + "discounted_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "is_removed": false, + "phone": null, + "price": "10.00", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Generic Shipping", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/paid", + "x-shopify-triggered-at": "2024-08-07T22:58:21.978733396Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/partially_fulfilled", + "name": "orders/partially_fulfilled", + "body": { + "id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/Order/820982911946154508", + "app_id": null, + "browser_ip": null, + "buyer_accepts_marketing": true, + "cancel_reason": "customer", + "cancelled_at": "2021-12-31T19:00:00-05:00", + "cart_token": null, + "checkout_id": null, + "checkout_token": null, + "client_details": null, + "closed_at": null, + "confirmation_number": null, + "confirmed": false, + "contact_email": "jon@example.com", + "created_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "current_subtotal_price": "398.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_additional_fees_set": null, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "current_total_duties_set": null, + "current_total_price": "398.00", + "current_total_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "customer_locale": "en", + "device_id": null, + "discount_codes": [], + "email": "jon@example.com", + "estimated_taxes": false, + "financial_status": "voided", + "fulfillment_status": "pending", + "landing_site": null, + "landing_site_ref": null, + "location_id": null, + "merchant_of_record_app_id": null, + "name": "#9999", + "note": null, + "note_attributes": [], + "number": 234, + "order_number": 1234, + "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + "original_total_additional_fees_set": null, + "original_total_duties_set": null, + "payment_gateway_names": [ + "visa", + "bogus" + ], + "phone": null, + "po_number": null, + "presentment_currency": "USD", + "processed_at": "2021-12-31T19:00:00-05:00", + "reference": null, + "referring_site": null, + "source_identifier": null, + "source_name": "web", + "source_url": null, + "subtotal_price": "388.00", + "subtotal_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "tags": "tag1, tag2", + "tax_exempt": false, + "tax_lines": [], + "taxes_included": false, + "test": true, + "token": "123456abcd", + "total_discounts": "20.00", + "total_discounts_set": { + "shop_money": { + "amount": "20.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "20.00", + "currency_code": "USD" + } + }, + "total_line_items_price": "398.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "total_outstanding": "398.00", + "total_price": "388.00", + "total_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "total_shipping_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2021-12-31T19:00:00-05:00", + "user_id": null, + "billing_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 115310627314723950, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "state": "disabled", + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "tags": "", + "currency": "USD", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954", + "default_address": { + "id": 715243470612851200, + "customer_id": 115310627314723950, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 866550311766439000, + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "attributed_staffs": [ + { + "id": "gid://shopify/StaffMember/902541635", + "quantity": 1 + } + ], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + }, + { + "id": 141249953214522980, + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "attributed_staffs": [], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "shipping_lines": [ + { + "id": 271878346596884000, + "carrier_identifier": null, + "code": null, + "discounted_price": "10.00", + "discounted_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "is_removed": false, + "phone": null, + "price": "10.00", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Generic Shipping", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/partially_fulfilled", + "x-shopify-triggered-at": "2024-08-07T22:58:27.151252909Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/risk_assessment_changed", + "name": "orders/risk_assessment_changed", + "body": { + "provider_id": null, + "provider_title": null, + "risk_level": "medium", + "created_at": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/risk_assessment_changed", + "x-shopify-triggered-at": "2024-08-07T22:58:32.591487627Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/shopify_protect_eligibility_changed", + "name": "orders/shopify_protect_eligibility_changed", + "body": { + "order_id": 1, + "status": "active", + "eligibility": { + "status": "eligible" + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/shopify_protect_eligibility_changed", + "x-shopify-triggered-at": "2024-08-07T22:58:38.030497101Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "orders/updated", + "name": "orders/updated", + "body": { + "id": 820982911946154500, + "admin_graphql_api_id": "gid://shopify/Order/820982911946154508", + "app_id": null, + "browser_ip": null, + "buyer_accepts_marketing": true, + "cancel_reason": "customer", + "cancelled_at": "2021-12-31T19:00:00-05:00", + "cart_token": null, + "checkout_id": null, + "checkout_token": null, + "client_details": null, + "closed_at": null, + "confirmation_number": null, + "confirmed": false, + "contact_email": "jon@example.com", + "created_at": "2021-12-31T19:00:00-05:00", + "currency": "USD", + "current_subtotal_price": "398.00", + "current_subtotal_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_additional_fees_set": null, + "current_total_discounts": "0.00", + "current_total_discounts_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "current_total_duties_set": null, + "current_total_price": "398.00", + "current_total_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "current_total_tax": "0.00", + "current_total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "customer_locale": "en", + "device_id": null, + "discount_codes": [], + "email": "jon@example.com", + "estimated_taxes": false, + "financial_status": "voided", + "fulfillment_status": "pending", + "landing_site": null, + "landing_site_ref": null, + "location_id": null, + "merchant_of_record_app_id": null, + "name": "#9999", + "note": null, + "note_attributes": [], + "number": 234, + "order_number": 1234, + "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + "original_total_additional_fees_set": null, + "original_total_duties_set": null, + "payment_gateway_names": [ + "visa", + "bogus" + ], + "phone": null, + "po_number": null, + "presentment_currency": "USD", + "processed_at": "2021-12-31T19:00:00-05:00", + "reference": null, + "referring_site": null, + "source_identifier": null, + "source_name": "web", + "source_url": null, + "subtotal_price": "388.00", + "subtotal_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "tags": "tag1, tag2", + "tax_exempt": false, + "tax_lines": [], + "taxes_included": false, + "test": true, + "token": "123456abcd", + "total_discounts": "20.00", + "total_discounts_set": { + "shop_money": { + "amount": "20.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "20.00", + "currency_code": "USD" + } + }, + "total_line_items_price": "398.00", + "total_line_items_price_set": { + "shop_money": { + "amount": "398.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "398.00", + "currency_code": "USD" + } + }, + "total_outstanding": "398.00", + "total_price": "388.00", + "total_price_set": { + "shop_money": { + "amount": "388.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "388.00", + "currency_code": "USD" + } + }, + "total_shipping_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "total_tax": "0.00", + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "total_tip_received": "0.00", + "total_weight": 0, + "updated_at": "2021-12-31T19:00:00-05:00", + "user_id": null, + "billing_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "customer": { + "id": 115310627314723950, + "email": "john@example.com", + "created_at": null, + "updated_at": null, + "first_name": "John", + "last_name": "Smith", + "state": "disabled", + "note": null, + "verified_email": true, + "multipass_identifier": null, + "tax_exempt": false, + "phone": null, + "email_marketing_consent": { + "state": "not_subscribed", + "opt_in_level": null, + "consent_updated_at": null + }, + "sms_marketing_consent": null, + "tags": "", + "currency": "USD", + "tax_exemptions": [], + "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954", + "default_address": { + "id": 715243470612851200, + "customer_id": 115310627314723950, + "first_name": null, + "last_name": null, + "company": null, + "address1": "123 Elm St.", + "address2": null, + "city": "Ottawa", + "province": "Ontario", + "country": "Canada", + "zip": "K2H7A8", + "phone": "123-123-1234", + "name": "", + "province_code": "ON", + "country_code": "CA", + "country_name": "Canada", + "default": true + } + }, + "discount_applications": [], + "fulfillments": [], + "line_items": [ + { + "id": 866550311766439000, + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "attributed_staffs": [ + { + "id": "gid://shopify/StaffMember/902541635", + "quantity": 1 + } + ], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + }, + { + "id": 141249953214522980, + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "attributed_staffs": [], + "current_quantity": 1, + "fulfillable_quantity": 1, + "fulfillment_service": "manual", + "fulfillment_status": null, + "gift_card": false, + "grams": 567, + "name": "IPod Nano - 8GB", + "price": "199.00", + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "product_exists": true, + "product_id": 632910392, + "properties": [], + "quantity": 1, + "requires_shipping": true, + "sku": "IPOD2008PINK", + "taxable": true, + "title": "IPod Nano - 8GB", + "total_discount": "0.00", + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "variant_id": 808950810, + "variant_inventory_management": "shopify", + "variant_title": null, + "vendor": null, + "tax_lines": [], + "duties": [], + "discount_allocations": [] + } + ], + "payment_terms": null, + "refunds": [], + "shipping_address": { + "first_name": "Steve", + "address1": "123 Shipping Street", + "phone": "555-555-SHIP", + "city": "Shippington", + "zip": "40003", + "province": "Kentucky", + "country": "United States", + "last_name": "Shipper", + "address2": null, + "company": "Shipping Company", + "latitude": null, + "longitude": null, + "name": "Steve Shipper", + "country_code": "US", + "province_code": "KY" + }, + "shipping_lines": [ + { + "id": 271878346596884000, + "carrier_identifier": null, + "code": null, + "discounted_price": "10.00", + "discounted_price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "is_removed": false, + "phone": null, + "price": "10.00", + "price_set": { + "shop_money": { + "amount": "10.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "10.00", + "currency_code": "USD" + } + }, + "requested_fulfillment_service_id": null, + "source": "shopify", + "title": "Generic Shipping", + "tax_lines": [], + "discount_allocations": [] + } + ] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "orders/updated", + "x-shopify-triggered-at": "2024-08-07T22:58:43.610785557Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "payment_schedules/due", + "name": "payment_schedules/due", + "body": { + "id": 606405506930370000, + "payment_terms_id": 706405506930370000, + "amount": "10.00", + "currency": "USD", + "issued_at": "2021-01-01T00:00:00-05:00", + "due_at": "2021-01-02T00:00:00-05:00", + "completed_at": "2021-01-02T00:00:00-05:00", + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "admin_graphql_api_id": "gid://shopify/PaymentSchedule/606405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "payment_schedules/due", + "x-shopify-triggered-at": "2024-08-07T22:58:54.463509482Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "payment_terms/create", + "name": "payment_terms/create", + "body": { + "id": 706405506930370000, + "payment_terms_name": "Net 7", + "payment_terms_type": "net", + "due_in_days": 7, + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "payment_schedules": [ + { + "id": 606405506930370000, + "payment_terms_id": 706405506930370000, + "amount": "10.00", + "currency": "USD", + "issued_at": "2021-01-01T00:00:00-05:00", + "due_at": "2021-01-02T00:00:00-05:00", + "completed_at": "2021-01-02T00:00:00-05:00", + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "admin_graphql_api_id": "gid://shopify/PaymentSchedule/606405506930370084" + } + ], + "admin_graphql_api_id": "gid://shopify/PaymentsPaymentFlexibilityPaymentTermsPaymentTerms/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "payment_terms/create", + "x-shopify-triggered-at": "2024-08-07T22:58:59.427458111Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "payment_terms/delete", + "name": "payment_terms/delete", + "body": { + "id": 706405506930370000 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "payment_terms/delete", + "x-shopify-triggered-at": "2024-08-07T22:59:04.792354399Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "payment_terms/update", + "name": "payment_terms/update", + "body": { + "id": 706405506930370000, + "payment_terms_name": "Net 7", + "payment_terms_type": "net", + "due_in_days": 7, + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "payment_schedules": [ + { + "id": 606405506930370000, + "payment_terms_id": 706405506930370000, + "amount": "10.00", + "currency": "USD", + "issued_at": "2021-01-01T00:00:00-05:00", + "due_at": "2021-01-02T00:00:00-05:00", + "completed_at": "2021-01-02T00:00:00-05:00", + "created_at": "2021-01-01T00:00:00-05:00", + "updated_at": "2021-01-01T00:00:01-05:00", + "admin_graphql_api_id": "gid://shopify/PaymentSchedule/606405506930370084" + } + ], + "admin_graphql_api_id": "gid://shopify/PaymentsPaymentFlexibilityPaymentTermsPaymentTerms/706405506930370084" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "payment_terms/update", + "x-shopify-triggered-at": "2024-08-07T22:59:10.596439947Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_feeds/create", + "name": "product_feeds/create", + "body": { + "id": "gid://shopify/ProductFeed/", + "country": "CA", + "language": "EN", + "status": "" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_feeds/create", + "x-shopify-triggered-at": "2024-08-07T22:59:31.722134805Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_feeds/full_sync", + "name": "product_feeds/full_sync", + "body": { + "metadata": { + "action": "CREATE", + "type": "FULL", + "resource": "PRODUCT", + "fullSyncId": "gid://shopify/ProductFullSync/1123511235", + "truncatedFields": [], + "occurred_at": "2022-01-01T00:00:00.000Z" + }, + "productFeed": { + "id": "gid://shopify/ProductFeed/12345", + "shop_id": "gid://shopify/Shop/12345", + "country": "CA", + "language": "EN" + }, + "product": { + "id": "gid://shopify/Product/12345", + "title": "Coffee", + "description": "The best coffee in the world", + "onlineStoreUrl": "https://example.com/products/coffee", + "createdAt": "2021-12-31T19:00:00-05:00", + "updatedAt": "2021-12-31T19:00:00-05:00", + "isPublished": true, + "publishedAt": "2021-12-31T19:00:00-05:00", + "productType": "Coffee", + "vendor": "Cawfee Inc", + "handle": "", + "images": { + "edges": [ + { + "node": { + "id": "gid://shopify/ProductImage/394", + "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", + "height": 3024, + "width": 4032 + } + } + ] + }, + "options": [ + { + "name": "Title", + "values": [ + "151cm", + "155cm", + "158cm" + ] + } + ], + "seo": { + "title": "seo title", + "description": "seo description" + }, + "tags": [ + "tag1", + "tag2" + ], + "variants": { + "edges": [ + { + "node": { + "id": "gid://shopify/ProductVariant/1", + "title": "151cm", + "price": { + "amount": "100.00", + "currencyCode": "CAD" + }, + "compareAtPrice": null, + "sku": "12345", + "barcode": null, + "quantityAvailable": 10, + "availableForSale": true, + "weight": 2.3, + "weightUnit": "KILOGRAMS", + "requireShipping": true, + "inventoryPolicy": "DENY", + "createdAt": "2021-12-31T19:00:00-05:00", + "updatedAt": "2021-12-31T19:00:00-05:00", + "image": { + "id": "gid://shopify/ProductImage/394", + "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", + "height": 3024, + "width": 4032 + }, + "selectedOptions": [ + { + "name": "Title", + "value": "151cm" + } + ] + } + } + ] + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_feeds/full_sync", + "x-shopify-triggered-at": "2024-08-07T22:59:40.683206202Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_feeds/full_sync_finish", + "name": "product_feeds/full_sync_finish", + "body": { + "metadata": { + "action": "CREATE", + "type": "FULL", + "resource": "PRODUCT", + "fullSyncId": "gid://shopify/ProductFullSync/1123511235", + "truncatedFields": [], + "occurred_at": "2022-01-01T00:00:00.000Z" + }, + "productFeed": { + "id": "gid://shopify/ProductFeed/12345", + "shop_id": "gid://shopify/Shop/12345", + "country": "CA", + "language": "EN" + }, + "fullSync": { + "createdAt": "2021-12-31 19:00:00 -0500", + "errorCode": null, + "status": "completed", + "count": 12, + "url": null + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_feeds/full_sync_finish", + "x-shopify-triggered-at": "2024-08-07T22:59:45.842464923Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_feeds/incremental_sync", + "name": "product_feeds/incremental_sync", + "body": { + "metadata": { + "action": "CREATE", + "type": "INCREMENTAL", + "resource": "PRODUCT", + "truncatedFields": [], + "occured_at": "2021-12-31T19:00:00-05:00" + }, + "productFeed": { + "id": "gid://shopify/ProductFeed/12345", + "shop_id": "gid://shopify/Shop/12345", + "country": "CA", + "language": "EN" + }, + "product": { + "id": "gid://shopify/Product/12345", + "title": "Coffee", + "description": "The best coffee in the world", + "onlineStoreUrl": "https://example.com/products/coffee", + "createdAt": "2021-12-31T19:00:00-05:00", + "updatedAt": "2021-12-31T19:00:00-05:00", + "isPublished": true, + "publishedAt": "2021-12-31T19:00:00-05:00", + "productType": "Coffee", + "vendor": "Cawfee Inc", + "handle": "", + "images": { + "edges": [ + { + "node": { + "id": "gid://shopify/ProductImage/394", + "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", + "height": 3024, + "width": 4032 + } + } + ] + }, + "options": [ + { + "name": "Title", + "values": [ + "151cm", + "155cm", + "158cm" + ] + } + ], + "seo": { + "title": "seo title", + "description": "seo description" + }, + "tags": [ + "tag1", + "tag2" + ], + "variants": { + "edges": [ + { + "node": { + "id": "gid://shopify/ProductVariant/1", + "title": "151cm", + "price": { + "amount": "100.00", + "currencyCode": "CAD" + }, + "compareAtPrice": null, + "sku": "12345", + "barcode": null, + "quantityAvailable": 10, + "availableForSale": true, + "weight": 2.3, + "weightUnit": "KILOGRAMS", + "requireShipping": true, + "inventoryPolicy": "DENY", + "createdAt": "2021-12-31T19:00:00-05:00", + "updatedAt": "2021-12-31T19:00:00-05:00", + "image": { + "id": "gid://shopify/ProductImage/394", + "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", + "height": 3024, + "width": 4032 + }, + "selectedOptions": [ + { + "name": "Title", + "value": "151cm" + } + ] + } + } + ] + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_feeds/incremental_sync", + "x-shopify-triggered-at": "2024-08-07T22:59:51.446208444Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_feeds/update", + "name": "product_feeds/update", + "body": { + "id": "gid://shopify/ProductFeed/", + "country": "CA", + "language": "EN", + "status": "" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_feeds/update", + "x-shopify-triggered-at": "2024-08-07T23:00:00.249354638Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_listings/add", + "name": "product_listings/add", + "body": { + "product_listing": { + "product_id": 788032119674292900, + "created_at": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "body_html": "An example T-Shirt", + "handle": "example-t-shirt", + "product_type": "Shirts", + "title": "Example T-Shirt", + "vendor": "Acme", + "available": true, + "tags": "example, mens, t-shirt", + "published_at": "2021-12-31T19:00:00-05:00", + "variants": [ + { + "id": 642667041472714000, + "title": "Small", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Small" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 1, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 75, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-30T19:00:00-05:00" + }, + { + "id": 757650484644203900, + "title": "Medium", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Medium" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 2, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 50, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00" + } + ], + "images": [], + "options": [ + { + "id": 527050010214937800, + "name": "Title", + "product_id": 788032119674292900, + "position": 1, + "values": [ + "Small", + "Medium" + ] + } + ] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_listings/add", + "x-shopify-triggered-at": "2024-08-07T23:00:06.009653743Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_listings/remove", + "name": "product_listings/remove", + "body": { + "product_listing": { + "product_id": 788032119674292900 + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_listings/remove", + "x-shopify-triggered-at": "2024-08-07T23:00:12.678096191Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_listings/update", + "name": "product_listings/update", + "body": { + "product_listing": { + "product_id": 788032119674292900, + "created_at": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "body_html": "An example T-Shirt", + "handle": "example-t-shirt", + "product_type": "Shirts", + "title": "Example T-Shirt", + "vendor": "Acme", + "available": true, + "tags": "example, mens, t-shirt", + "published_at": "2021-12-31T19:00:00-05:00", + "variants": [ + { + "id": 642667041472714000, + "title": "Small", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Small" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 1, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 75, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-30T19:00:00-05:00" + }, + { + "id": 757650484644203900, + "title": "Medium", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Medium" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 2, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 50, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00" + } + ], + "images": [], + "options": [ + { + "id": 527050010214937800, + "name": "Title", + "product_id": 788032119674292900, + "position": 1, + "values": [ + "Small", + "Medium" + ] + } + ] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_listings/update", + "x-shopify-triggered-at": "2024-08-07T23:00:18.530397876Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_publications/create", + "name": "product_publications/create", + "body": { + "id": null, + "publication_id": null, + "published_at": "2021-12-31T19:00:00-05:00", + "published": true, + "created_at": null, + "updated_at": null, + "product_id": 788032119674292900 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_publications/create", + "x-shopify-triggered-at": "2024-08-07T23:00:31.985927796Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_publications/delete", + "name": "product_publications/delete", + "body": { + "id": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_publications/delete", + "x-shopify-triggered-at": "2024-08-07T23:00:37.591429860Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "product_publications/update", + "name": "product_publications/update", + "body": { + "id": null, + "publication_id": null, + "published_at": "2021-12-31T19:00:00-05:00", + "published": true, + "created_at": null, + "updated_at": null, + "product_id": 788032119674292900 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "product_publications/update", + "x-shopify-triggered-at": "2024-08-07T23:00:42.927374952Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "products/create", + "name": "products/create", + "body": { + "admin_graphql_api_id": "gid://shopify/Product/788032119674292922", + "body_html": "An example T-Shirt", + "created_at": null, + "handle": "example-t-shirt", + "id": 788032119674292900, + "product_type": "Shirts", + "published_at": "2021-12-31T19:00:00-05:00", + "template_suffix": null, + "title": "Example T-Shirt", + "updated_at": "2021-12-31T19:00:00-05:00", + "vendor": "Acme", + "status": "active", + "published_scope": "web", + "tags": "example, mens, t-shirt", + "variants": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/642667041472713922", + "barcode": null, + "compare_at_price": "24.99", + "created_at": "2021-12-29T19:00:00-05:00", + "id": 642667041472714000, + "inventory_policy": "deny", + "position": 1, + "price": "19.99", + "product_id": 788032119674292900, + "sku": "example-shirt-s", + "taxable": true, + "title": "Small", + "updated_at": "2021-12-30T19:00:00-05:00", + "option1": "Small", + "option2": null, + "option3": null, + "image_id": null, + "inventory_item_id": null, + "inventory_quantity": 75, + "old_inventory_quantity": 75 + }, + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/757650484644203962", + "barcode": null, + "compare_at_price": "24.99", + "created_at": "2021-12-29T19:00:00-05:00", + "id": 757650484644203900, + "inventory_policy": "deny", + "position": 2, + "price": "19.99", + "product_id": 788032119674292900, + "sku": "example-shirt-m", + "taxable": true, + "title": "Medium", + "updated_at": "2021-12-31T19:00:00-05:00", + "option1": "Medium", + "option2": null, + "option3": null, + "image_id": null, + "inventory_item_id": null, + "inventory_quantity": 50, + "old_inventory_quantity": 50 + } + ], + "options": [], + "images": [], + "image": null, + "media": [], + "variant_gids": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/757650484644203962", + "updated_at": "2022-01-01T00:00:00.000Z" + }, + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/642667041472713922", + "updated_at": "2021-12-31T00:00:00.000Z" + } + ], + "has_variants_that_requires_components": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "products/create", + "x-shopify-triggered-at": "2024-08-07T22:59:15.982741430Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "products/delete", + "name": "products/delete", + "body": { + "id": 788032119674292900 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "products/delete", + "x-shopify-triggered-at": "2024-08-07T22:59:21.243809007Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "products/update", + "name": "products/update", + "body": { + "admin_graphql_api_id": "gid://shopify/Product/788032119674292922", + "body_html": "An example T-Shirt", + "created_at": null, + "handle": "example-t-shirt", + "id": 788032119674292900, + "product_type": "Shirts", + "published_at": "2021-12-31T19:00:00-05:00", + "template_suffix": null, + "title": "Example T-Shirt", + "updated_at": "2021-12-31T19:00:00-05:00", + "vendor": "Acme", + "status": "active", + "published_scope": "web", + "tags": "example, mens, t-shirt", + "variants": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/642667041472713922", + "barcode": null, + "compare_at_price": "24.99", + "created_at": "2021-12-29T19:00:00-05:00", + "id": 642667041472714000, + "inventory_policy": "deny", + "position": 1, + "price": "19.99", + "product_id": 788032119674292900, + "sku": "example-shirt-s", + "taxable": true, + "title": "Small", + "updated_at": "2021-12-30T19:00:00-05:00", + "option1": "Small", + "option2": null, + "option3": null, + "image_id": null, + "inventory_item_id": null, + "inventory_quantity": 75, + "old_inventory_quantity": 75 + }, + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/757650484644203962", + "barcode": null, + "compare_at_price": "24.99", + "created_at": "2021-12-29T19:00:00-05:00", + "id": 757650484644203900, + "inventory_policy": "deny", + "position": 2, + "price": "19.99", + "product_id": 788032119674292900, + "sku": "example-shirt-m", + "taxable": true, + "title": "Medium", + "updated_at": "2021-12-31T19:00:00-05:00", + "option1": "Medium", + "option2": null, + "option3": null, + "image_id": null, + "inventory_item_id": null, + "inventory_quantity": 50, + "old_inventory_quantity": 50 + } + ], + "options": [], + "images": [], + "image": null, + "media": [], + "variant_gids": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/757650484644203962", + "updated_at": "2022-01-01T00:00:00.000Z" + }, + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/642667041472713922", + "updated_at": "2021-12-31T00:00:00.000Z" + } + ], + "has_variants_that_requires_components": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "products/update", + "x-shopify-triggered-at": "2024-08-07T22:59:26.372189411Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "profiles/create", + "name": "profiles/create", + "body": { + "id": 1 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "profiles/create", + "x-shopify-triggered-at": "2024-08-07T23:00:48.411909237Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "profiles/delete", + "name": "profiles/delete", + "body": { + "id": 1 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "profiles/delete", + "x-shopify-triggered-at": "2024-08-07T23:00:53.839947655Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "profiles/update", + "name": "profiles/update", + "body": { + "id": 1 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "profiles/update", + "x-shopify-triggered-at": "2024-08-07T23:00:59.136451962Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "refunds/create", + "name": "refunds/create", + "body": { + "id": 890088186047892400, + "order_id": 820982911946154500, + "created_at": "2021-12-31T19:00:00-05:00", + "note": "Things were damaged", + "user_id": 548380009, + "processed_at": "2021-12-31T19:00:00-05:00", + "restock": false, + "duties": [], + "total_duties_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "return": null, + "refund_shipping_lines": [], + "admin_graphql_api_id": "gid://shopify/Refund/890088186047892319", + "order_adjustments": [], + "refund_line_items": [ + { + "id": 866550311766439000, + "quantity": 1, + "line_item_id": 866550311766439000, + "location_id": null, + "restock_type": "no_restock", + "subtotal": 199, + "total_tax": 0, + "subtotal_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "line_item": { + "id": 866550311766439000, + "variant_id": 808950810, + "title": "IPod Nano - 8GB", + "quantity": 1, + "sku": "IPOD2008PINK", + "variant_title": null, + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8GB", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 567, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/866550311766439020", + "tax_lines": [] + } + }, + { + "id": 141249953214523040, + "quantity": 1, + "line_item_id": 141249953214522980, + "location_id": null, + "restock_type": "no_restock", + "subtotal": 199, + "total_tax": 0, + "subtotal_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "line_item": { + "id": 141249953214522980, + "variant_id": 808950810, + "title": "IPod Nano - 8GB", + "quantity": 1, + "sku": "IPOD2008PINK", + "variant_title": null, + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8GB", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 567, + "price": "199.00", + "total_discount": "5.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "5.00", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + } + }, + { + "amount": "5.00", + "discount_application_index": 2, + "amount_set": { + "shop_money": { + "amount": "5.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "5.00", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/141249953214522974", + "tax_lines": [] + } + } + ], + "transactions": [] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "refunds/create", + "x-shopify-triggered-at": "2024-08-07T23:01:26.516907657Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/approve", + "name": "returns/approve", + "body": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "status": "open", + "order": { + "id": 4783296544821, + "admin_graphql_api_id": "gid://shopify/Order/4783296544821" + }, + "total_return_line_items": 2, + "name": null, + "return_line_items": [ + { + "id": 987654321, + "admin_graphql_api_id": "gid://shopify/ReturnLineItem/987654321", + "fulfillment_line_item": { + "id": 4463854417760, + "admin_graphql_api_id": "gid://shopify/FulfillmentLineItem/4463854417760", + "line_item": { + "id": 4176556606960, + "admin_graphql_api_id": "gid://shopify/LineItem/4176556606960" + } + }, + "quantity": 1, + "return_reason": "size_too_small", + "return_reason_note": "", + "customer_note": null, + "restocking_fee": { + "id": 283749348573489, + "admin_graphql_api_id": "gid://shopify/RestockingFee/283749348573489", + "price": { + "shop_money": { + "amount": "10.0", + "currency_code": "XXX" + }, + "presentment_money": { + "amount": "10.0", + "currency_code": "XXX" + } + } + } + }, + { + "id": 987654322, + "admin_graphql_api_id": "gid://shopify/ReturnLineItem/987654322", + "fulfillment_line_item": { + "id": 7219216693661, + "admin_graphql_api_id": "gid://shopify/FulfillmentLineItem/7219216693661", + "line_item": { + "id": 7014063798651, + "admin_graphql_api_id": "gid://shopify/LineItem/7014063798651" + } + }, + "quantity": 1, + "return_reason": "size_too_large", + "return_reason_note": "", + "customer_note": null, + "restocking_fee": null + } + ], + "return_shipping_fees": [ + { + "id": 987654321547866, + "admin_graphql_api_id": "gid://shopify/ReturnShippingFee/987654321547866", + "price": { + "shop_money": { + "amount": "10.0", + "currency_code": "XXX" + }, + "presentment_money": { + "amount": "10.0", + "currency_code": "XXX" + } + } + } + ], + "exchange_line_items": [ + { + "id": 987654321283476, + "admin_graphql_api_id": "gid://shopify/ExchangeLineItem/987654321283476", + "line_item": null + } + ], + "total_exchange_line_items": 1 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/approve", + "x-shopify-triggered-at": "2024-08-07T23:01:31.834007904Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/cancel", + "name": "returns/cancel", + "body": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "order_id": 4783296544821, + "status": "canceled" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/cancel", + "x-shopify-triggered-at": "2024-08-07T23:01:37.414567890Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/close", + "name": "returns/close", + "body": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "order_id": 4783296544821, + "status": "closed" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/close", + "x-shopify-triggered-at": "2024-08-07T23:01:43.121775055Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/decline", + "name": "returns/decline", + "body": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "status": "declined", + "decline": { + "reason": "return_period_ended", + "note": "As discussed on the phone, the 30-day return window has ended." + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/decline", + "x-shopify-triggered-at": "2024-08-07T23:01:48.650976147Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/reopen", + "name": "returns/reopen", + "body": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "order_id": 4783296544821, + "status": "open" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/reopen", + "x-shopify-triggered-at": "2024-08-07T23:01:53.976682665Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/request", + "name": "returns/request", + "body": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "status": "requested", + "order": { + "id": 4783296544821, + "admin_graphql_api_id": "gid://shopify/Order/4783296544821" + }, + "total_return_line_items": 2, + "name": null, + "return_line_items": [ + { + "id": 987654321, + "admin_graphql_api_id": "gid://shopify/ReturnLineItem/987654321", + "fulfillment_line_item": { + "id": 4463854417760, + "admin_graphql_api_id": "gid://shopify/FulfillmentLineItem/4463854417760", + "line_item": { + "id": 4176556606960, + "admin_graphql_api_id": "gid://shopify/LineItem/4176556606960" + } + }, + "quantity": 1, + "return_reason": "size_too_small", + "return_reason_note": "", + "customer_note": null, + "restocking_fee": null + }, + { + "id": 987654322, + "admin_graphql_api_id": "gid://shopify/ReturnLineItem/987654322", + "fulfillment_line_item": { + "id": 7219216693661, + "admin_graphql_api_id": "gid://shopify/FulfillmentLineItem/7219216693661", + "line_item": { + "id": 7014063798651, + "admin_graphql_api_id": "gid://shopify/LineItem/7014063798651" + } + }, + "quantity": 1, + "return_reason": "size_too_large", + "return_reason_note": "", + "customer_note": null, + "restocking_fee": null + } + ], + "return_shipping_fees": [], + "exchange_line_items": [], + "total_exchange_line_items": 0 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/request", + "x-shopify-triggered-at": "2024-08-07T23:01:59.160232984Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "returns/update", + "name": "returns/update", + "body": { + "admin_graphql_api_id": "gid://shopify/Return/123134564567890", + "return_line_items": { + "removals": [ + { + "admin_graphql_api_id": "gid://shopify/ReturnLineItem/987654321", + "delta": 2 + } + ] + }, + "restocking_fees": { + "updates": [], + "removals": [] + }, + "return_shipping_fees": { + "updates": [], + "removals": [] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "returns/update", + "x-shopify-triggered-at": "2024-08-07T23:02:04.309153593Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "reverse_deliveries/attach_deliverable", + "name": "reverse_deliveries/attach_deliverable", + "body": { + "id": 9080907654321, + "admin_graphql_api_id": "gid://shopify/ReverseDelivery/9080907654321", + "return": { + "id": 123134564567890, + "admin_graphql_api_id": "gid://shopify/Return/123134564567890" + }, + "shipping_deliverable": { + "tracking": { + "carrier_name": "USPS", + "tracking_number": "123345345", + "tracking_url": null + }, + "label": { + "public_file_url": null, + "created_at": null + } + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "reverse_deliveries/attach_deliverable", + "x-shopify-triggered-at": "2024-08-07T23:02:10.185338627Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "reverse_fulfillment_orders/dispose", + "name": "reverse_fulfillment_orders/dispose", + "body": { + "id": 11111111, + "admin_graphql_api_id": "gid://shopify/ReverseFulfillmentOrder/11111111", + "dispositions": [ + { + "reverse_fulfillment_order_line_item": { + "id": 2222222, + "admin_graphql_api_id": "gid://shopify/ReverseFulfillmentOrderLineItem/2222222" + }, + "reverse_delivery_line_item": null, + "type": "restocked", + "location": { + "id": 3333333, + "admin_graphql_api_id": "gid://shopify/Location/3333333" + }, + "quantity": 1 + }, + { + "reverse_fulfillment_order_line_item": { + "id": 2222223, + "admin_graphql_api_id": "gid://shopify/ReverseFulfillmentOrderLineItem/2222223" + }, + "reverse_delivery_line_item": null, + "type": "restocked", + "location": { + "id": 3333333, + "admin_graphql_api_id": "gid://shopify/Location/3333333" + }, + "quantity": 1 + } + ], + "total_dispositions": 2 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "reverse_fulfillment_orders/dispose", + "x-shopify-triggered-at": "2024-08-07T23:02:15.745485629Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "scheduled_product_listings/add", + "name": "scheduled_product_listings/add", + "body": { + "scheduled_product_listing": { + "product_id": 788032119674292900, + "created_at": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "body_html": "An example T-Shirt", + "handle": "example-t-shirt", + "product_type": "Shirts", + "title": "Example T-Shirt", + "vendor": "Acme", + "available": true, + "tags": "example, mens, t-shirt", + "variants": [ + { + "id": 642667041472714000, + "title": "Small", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Small" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 1, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 75, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-30T19:00:00-05:00" + }, + { + "id": 757650484644203900, + "title": "Medium", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Medium" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 2, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 50, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00" + } + ], + "publish_at": null, + "images": [], + "options": [ + { + "id": 527050010214937800, + "name": "Title", + "product_id": 788032119674292900, + "position": 1, + "values": [ + "Small", + "Medium" + ] + } + ] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "scheduled_product_listings/add", + "x-shopify-triggered-at": "2024-08-07T23:02:21.589526157Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "scheduled_product_listings/remove", + "name": "scheduled_product_listings/remove", + "body": { + "scheduled_product_listing": { + "product_id": 788032119674292900 + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "scheduled_product_listings/remove", + "x-shopify-triggered-at": "2024-08-07T23:02:26.822698262Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "scheduled_product_listings/update", + "name": "scheduled_product_listings/update", + "body": { + "scheduled_product_listing": { + "product_id": 788032119674292900, + "created_at": null, + "updated_at": "2021-12-31T19:00:00-05:00", + "body_html": "An example T-Shirt", + "handle": "example-t-shirt", + "product_type": "Shirts", + "title": "Example T-Shirt", + "vendor": "Acme", + "available": true, + "tags": "example, mens, t-shirt", + "variants": [ + { + "id": 642667041472714000, + "title": "Small", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Small" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 1, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 75, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-30T19:00:00-05:00" + }, + { + "id": 757650484644203900, + "title": "Medium", + "option_values": [ + { + "option_id": 527050010214937800, + "name": "Title", + "value": "Medium" + } + ], + "price": "19.99", + "formatted_price": "$19.99", + "compare_at_price": "24.99", + "grams": 200, + "requires_shipping": true, + "sku": null, + "barcode": null, + "taxable": true, + "position": 2, + "available": true, + "inventory_policy": "deny", + "inventory_quantity": 50, + "inventory_management": null, + "fulfillment_service": "manual", + "weight": null, + "weight_unit": "lb", + "image_id": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00" + } + ], + "publish_at": null, + "images": [], + "options": [ + { + "id": 527050010214937800, + "name": "Title", + "product_id": 788032119674292900, + "position": 1, + "values": [ + "Small", + "Medium" + ] + } + ] + } + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "scheduled_product_listings/update", + "x-shopify-triggered-at": "2024-08-07T23:02:32.488713857Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "segments/create", + "name": "segments/create", + "body": { + "id": 1, + "name": "Customers who have one order", + "query": "number_of_orders = 1", + "creationDate": "2022-01-01T00:00:00.000Z", + "lastEditDate": "2022-01-01T00:00:00.000Z" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "segments/create", + "x-shopify-triggered-at": "2024-08-07T23:02:37.769131380Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "segments/delete", + "name": "segments/delete", + "body": { + "id": 1 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "segments/delete", + "x-shopify-triggered-at": "2024-08-07T23:02:43.121330242Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "segments/update", + "name": "segments/update", + "body": { + "id": 1, + "name": "Customers who have one order", + "query": "number_of_orders = 1", + "creationDate": "2005-05-05T00:00:00.000Z", + "lastEditDate": "2022-01-01T00:00:00.000Z" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "segments/update", + "x-shopify-triggered-at": "2024-08-07T23:02:48.168986480Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "selling_plan_groups/create", + "name": "selling_plan_groups/create", + "body": { + "admin_graphql_api_id": "gid://shopify/SellingPlanGroup/1039518907", + "id": 1039518907, + "name": "Subscribe & Save", + "merchant_code": "sub-n-save", + "admin_graphql_api_app": "gid://shopify/App/2525000003", + "app_id": null, + "description": null, + "options": [ + "Delivery every" + ], + "position": null, + "summary": "1 delivery frequency, discount", + "selling_plans": [ + { + "name": "Pay every month deliver every month", + "options": [ + "month" + ], + "position": null, + "description": null, + "billing_policy": { + "interval": "month", + "interval_count": 1, + "min_cycles": null, + "max_cycles": null + }, + "delivery_policy": { + "interval": "month", + "interval_count": 1, + "anchors": [], + "cutoff": null, + "pre_anchor_behavior": "asap" + }, + "pricing_policies": [] + } + ], + "product_variants": [], + "products": [] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "selling_plan_groups/create", + "x-shopify-triggered-at": "2024-08-07T23:02:53.330801847Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "selling_plan_groups/delete", + "name": "selling_plan_groups/delete", + "body": { + "admin_graphql_api_id": "gid://shopify/SellingPlanGroup/1039518908", + "id": 1039518908 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "selling_plan_groups/delete", + "x-shopify-triggered-at": "2024-08-07T23:02:58.719142003Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "selling_plan_groups/update", + "name": "selling_plan_groups/update", + "body": { + "admin_graphql_api_id": "gid://shopify/SellingPlanGroup/1039518918", + "id": 1039518918, + "name": "Subscribe & Save", + "merchant_code": "sub-n-save", + "admin_graphql_api_app": "gid://shopify/App/2525000003", + "app_id": null, + "description": null, + "options": [ + "Delivery every" + ], + "position": null, + "summary": "1 delivery frequency, discount", + "selling_plans": [ + { + "name": "Pay every month deliver every month", + "options": [ + "month" + ], + "position": null, + "description": null, + "billing_policy": { + "interval": "month", + "interval_count": 1, + "min_cycles": null, + "max_cycles": null + }, + "delivery_policy": { + "interval": "month", + "interval_count": 1, + "anchors": [], + "cutoff": null, + "pre_anchor_behavior": "asap" + }, + "pricing_policies": [] + } + ], + "product_variants": [], + "products": [] + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "selling_plan_groups/update", + "x-shopify-triggered-at": "2024-08-07T23:03:04.118351440Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "shipping_addresses/create", + "name": "shipping_addresses/create", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "shipping_addresses/create", + "x-shopify-triggered-at": "2024-08-07T23:03:09.447035088Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "shipping_addresses/update", + "name": "shipping_addresses/update", + "body": {}, + "extraHeaders": { + "transfer-encoding": "chunked", + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "shipping_addresses/update", + "x-shopify-triggered-at": "2024-08-07T23:03:16.443104959Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "shop/update", + "name": "shop/update", + "body": { + "id": 548380009, + "name": "Super Toys", + "email": "super@supertoys.com", + "domain": null, + "province": "Tennessee", + "country": "US", + "address1": "190 MacLaren Street", + "zip": "37178", + "city": "Houston", + "source": null, + "phone": "3213213210", + "latitude": null, + "longitude": null, + "primary_locale": "en", + "address2": null, + "created_at": null, + "updated_at": null, + "country_code": "US", + "country_name": "United States", + "currency": "USD", + "customer_email": "super@supertoys.com", + "timezone": "(GMT-05:00) Eastern Time (US & Canada)", + "iana_timezone": null, + "shop_owner": "John Smith", + "money_format": "${{amount}}", + "money_with_currency_format": "${{amount}} USD", + "weight_unit": "kg", + "province_code": "TN", + "taxes_included": null, + "auto_configure_tax_inclusivity": null, + "tax_shipping": null, + "county_taxes": null, + "plan_display_name": "Shopify Plus", + "plan_name": "enterprise", + "has_discounts": false, + "has_gift_cards": true, + "myshopify_domain": null, + "google_apps_domain": null, + "google_apps_login_enabled": null, + "money_in_emails_format": "${{amount}}", + "money_with_currency_in_emails_format": "${{amount}} USD", + "eligible_for_payments": true, + "requires_extra_payments_agreement": false, + "password_enabled": null, + "has_storefront": true, + "finances": true, + "primary_location_id": 655441491, + "checkout_api_supported": true, + "multi_location_enabled": true, + "setup_required": false, + "pre_launch_enabled": false, + "enabled_presentment_currencies": [ + "USD" + ], + "marketing_sms_consent_enabled_at_checkout": false, + "transactional_sms_disabled": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "shop/update", + "x-shopify-triggered-at": "2024-08-07T23:03:21.884273252Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_attempts/challenged", + "name": "subscription_billing_attempts/challenged", + "body": { + "id": null, + "admin_graphql_api_id": null, + "idempotency_key": "9a453d81-d41d-403e-806f-714dee215ff9", + "order_id": 1, + "admin_graphql_api_order_id": "gid://shopify/Order/1", + "subscription_contract_id": 9601391618, + "admin_graphql_api_subscription_contract_id": "gid://shopify/SubscriptionContract/9601391618", + "ready": true, + "error_message": null, + "error_code": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_attempts/challenged", + "x-shopify-triggered-at": "2024-08-07T23:03:27.142015031Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_attempts/failure", + "name": "subscription_billing_attempts/failure", + "body": { + "id": null, + "admin_graphql_api_id": null, + "idempotency_key": "9a453d81-d41d-403e-806f-714dee215ff9", + "order_id": 1, + "admin_graphql_api_order_id": "gid://shopify/Order/1", + "subscription_contract_id": 9928822889, + "admin_graphql_api_subscription_contract_id": "gid://shopify/SubscriptionContract/9928822889", + "ready": true, + "error_message": null, + "error_code": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_attempts/failure", + "x-shopify-triggered-at": "2024-08-07T23:03:32.395340617Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_attempts/success", + "name": "subscription_billing_attempts/success", + "body": { + "id": null, + "admin_graphql_api_id": null, + "idempotency_key": "9a453d81-d41d-403e-806f-714dee215ff9", + "order_id": 1, + "admin_graphql_api_order_id": "gid://shopify/Order/1", + "subscription_contract_id": 8712043388, + "admin_graphql_api_subscription_contract_id": "gid://shopify/SubscriptionContract/8712043388", + "ready": true, + "error_message": null, + "error_code": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_attempts/success", + "x-shopify-triggered-at": "2024-08-07T23:03:37.594388878Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_cycle_edits/create", + "name": "subscription_billing_cycle_edits/create", + "body": { + "subscription_contract_id": 2089608828, + "cycle_start_at": "2022-10-01T00:00:00-04:00", + "cycle_end_at": "2022-11-01T00:00:00-04:00", + "cycle_index": 1, + "contract_edit": null, + "billing_attempt_expected_date": "2022-11-01T00:00:00-04:00", + "skipped": false, + "edited": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_cycle_edits/create", + "x-shopify-triggered-at": "2024-08-07T23:03:53.358117796Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_cycle_edits/delete", + "name": "subscription_billing_cycle_edits/delete", + "body": { + "subscription_contract_id": 8967220567, + "cycle_start_at": "2022-10-01T00:00:00-04:00", + "cycle_end_at": "2022-11-01T00:00:00-04:00", + "cycle_index": 1, + "contract_edit": null, + "billing_attempt_expected_date": "2022-11-01T00:00:00-04:00", + "skipped": false, + "edited": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_cycle_edits/delete", + "x-shopify-triggered-at": "2024-08-07T23:03:58.747170360Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_cycle_edits/update", + "name": "subscription_billing_cycle_edits/update", + "body": { + "subscription_contract_id": 3383495807, + "cycle_start_at": "2022-10-01T00:00:00-04:00", + "cycle_end_at": "2022-11-01T00:00:00-04:00", + "cycle_index": 1, + "contract_edit": null, + "billing_attempt_expected_date": "2022-11-01T00:00:00-04:00", + "skipped": false, + "edited": false + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_cycle_edits/update", + "x-shopify-triggered-at": "2024-08-07T23:04:04.593178392Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_cycles/skip", + "name": "subscription_billing_cycles/skip", + "body": { + "subscription_contract_id": 6216896094, + "cycle_start_at": "2022-10-01T00:00:00-04:00", + "cycle_end_at": "2022-11-01T00:00:00-04:00", + "cycle_index": 1, + "contract_edit": null, + "billing_attempt_expected_date": "2022-11-01T00:00:00-04:00", + "skipped": true, + "edited": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_cycles/skip", + "x-shopify-triggered-at": "2024-08-07T23:03:42.860287603Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_billing_cycles/unskip", + "name": "subscription_billing_cycles/unskip", + "body": { + "subscription_contract_id": 9448952952, + "cycle_start_at": "2022-10-01T00:00:00-04:00", + "cycle_end_at": "2022-11-01T00:00:00-04:00", + "cycle_index": 1, + "contract_edit": null, + "billing_attempt_expected_date": "2022-11-01T00:00:00-04:00", + "skipped": false, + "edited": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_billing_cycles/unskip", + "x-shopify-triggered-at": "2024-08-07T23:03:48.346695038Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/activate", + "name": "subscription_contracts/activate", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/8144725024", + "id": 8144725024, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "active", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "6734576965" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/activate", + "x-shopify-triggered-at": "2024-08-07T23:04:10.231356281Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/cancel", + "name": "subscription_contracts/cancel", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/8879965539", + "id": 8879965539, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "cancelled", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "7654492114" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/cancel", + "x-shopify-triggered-at": "2024-08-07T23:04:15.455046945Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/create", + "name": "subscription_contracts/create", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/2130602109", + "id": 2130602109, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "active", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "5735405187" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/create", + "x-shopify-triggered-at": "2024-08-07T23:04:20.428915161Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/expire", + "name": "subscription_contracts/expire", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/3504917051", + "id": 3504917051, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "expired", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "7780548242" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/expire", + "x-shopify-triggered-at": "2024-08-07T23:04:26.319892364Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/fail", + "name": "subscription_contracts/fail", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/5483094388", + "id": 5483094388, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "failed", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "8086755151" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/fail", + "x-shopify-triggered-at": "2024-08-07T23:04:31.543127745Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/pause", + "name": "subscription_contracts/pause", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/8004927509", + "id": 8004927509, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "paused", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "1393248951" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/pause", + "x-shopify-triggered-at": "2024-08-07T23:04:37.111638027Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "subscription_contracts/update", + "name": "subscription_contracts/update", + "body": { + "admin_graphql_api_id": "gid://shopify/SubscriptionContract/8249570610", + "id": 8249570610, + "billing_policy": { + "interval": "week", + "interval_count": 4, + "min_cycles": 1, + "max_cycles": 2 + }, + "currency_code": "USD", + "customer_id": 1, + "admin_graphql_api_customer_id": "gid://shopify/Customer/1", + "delivery_policy": { + "interval": "week", + "interval_count": 2 + }, + "status": "active", + "admin_graphql_api_origin_order_id": "gid://shopify/Order/1", + "origin_order_id": 1, + "revision_id": "5132632722" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "subscription_contracts/update", + "x-shopify-triggered-at": "2024-08-07T23:04:42.270520870Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "tax_services/create", + "name": "tax_services/create", + "body": { + "id": null, + "name": "Tax Service", + "url": "https://taxes.shopify.com", + "active": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "tax_services/create", + "x-shopify-triggered-at": "2024-08-07T23:05:02.595708933Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "tax_services/update", + "name": "tax_services/update", + "body": { + "id": null, + "name": "Tax Service", + "url": "https://taxes.shopify.com", + "active": true + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "tax_services/update", + "x-shopify-triggered-at": "2024-08-07T23:05:08.762628798Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "tender_transactions/create", + "name": "tender_transactions/create", + "body": { + "id": 220982911946154500, + "order_id": 820982911946154500, + "amount": "403.00", + "currency": "USD", + "user_id": null, + "test": false, + "processed_at": null, + "remote_reference": "1001", + "payment_details": null, + "payment_method": "unknown" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "tender_transactions/create", + "x-shopify-triggered-at": "2024-08-07T23:05:17.607923518Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "themes/create", + "name": "themes/create", + "body": { + "id": 512162865275216960, + "name": "Comfort", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "role": "main", + "theme_store_id": 1234, + "previewable": true, + "processing": false, + "admin_graphql_api_id": "gid://shopify/Theme/512162865275216980" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "themes/create", + "x-shopify-triggered-at": "2024-08-07T23:05:23.115137891Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "themes/delete", + "name": "themes/delete", + "body": { + "id": 512162865275216960 + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "themes/delete", + "x-shopify-triggered-at": "2024-08-07T23:05:28.399540399Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "themes/publish", + "name": "themes/publish", + "body": { + "id": 512162865275216960, + "name": "Comfort", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "role": "main", + "theme_store_id": 1234, + "previewable": true, + "processing": false, + "admin_graphql_api_id": "gid://shopify/Theme/512162865275216980" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "themes/publish", + "x-shopify-triggered-at": "2024-08-07T23:05:33.743928137Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "themes/update", + "name": "themes/update", + "body": { + "id": 512162865275216960, + "name": "Comfort", + "created_at": "2021-12-31T19:00:00-05:00", + "updated_at": "2021-12-31T19:00:00-05:00", + "role": "main", + "theme_store_id": 1234, + "previewable": true, + "processing": false, + "admin_graphql_api_id": "gid://shopify/Theme/512162865275216980" + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "themes/update", + "x-shopify-triggered-at": "2024-08-07T23:05:39.099831671Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "variants/in_stock", + "name": "variants/in_stock", + "body": { + "id": 642667041472714000, + "product_id": 788032119674292900, + "title": "Small", + "price": "19.99", + "sku": "example-shirt-s", + "position": 1, + "inventory_policy": "deny", + "compare_at_price": "24.99", + "option1": "Small", + "option2": null, + "option3": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-30T19:00:00-05:00", + "taxable": true, + "barcode": null, + "inventory_quantity": 75, + "old_inventory_quantity": 75, + "admin_graphql_api_id": "gid://shopify/ProductVariant/642667041472713922", + "image_id": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "variants/in_stock", + "x-shopify-triggered-at": "2024-08-07T23:05:51.902368158Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "shopify", + "providerLabel": "Shopify", + "eventType": "variants/out_of_stock", + "name": "variants/out_of_stock", + "body": { + "id": 642667041472714000, + "product_id": 788032119674292900, + "title": "Small", + "price": "19.99", + "sku": "example-shirt-s", + "position": 1, + "inventory_policy": "deny", + "compare_at_price": "24.99", + "option1": "Small", + "option2": null, + "option3": null, + "created_at": "2021-12-29T19:00:00-05:00", + "updated_at": "2021-12-30T19:00:00-05:00", + "taxable": true, + "barcode": null, + "inventory_quantity": 0, + "old_inventory_quantity": 0, + "admin_graphql_api_id": "gid://shopify/ProductVariant/642667041472713922", + "image_id": null + }, + "extraHeaders": { + "x-shopify-api-version": "2024-10", + "x-shopify-shop-domain": "shop.myshopify.com", + "x-shopify-test": "true", + "x-shopify-topic": "variants/out_of_stock", + "x-shopify-triggered-at": "2024-08-07T23:08:54.875833943Z" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:shopify/2024-10", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "checkout.session.completed", + "name": "checkout.session.completed", + "body": { + "id": "evt_1MhUT6E0b6fckueSlc4GyvIi", + "object": "event", + "api_version": "2022-11-15", + "created": 1677834000, + "data": { + "object": { + "id": "cs_test_a1CagZ1XmCLIZb3ME6r06G1h8ygwbbbMw8Nqk0R86MWMT4dmCjDiUYfQDr", + "object": "checkout.session", + "after_expiration": null, + "allow_promotion_codes": false, + "amount_subtotal": 0, + "amount_total": 0, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_address_collection": "auto", + "cancel_url": "https://stripe.com", + "client_reference_id": null, + "consent": null, + "consent_collection": { + "promotions": "none", + "terms_of_service": "none" + }, + "created": 1677833849, + "currency": "usd", + "custom_fields": [], + "custom_text": { + "shipping_address": null, + "submit": null + }, + "customer": "cus_NSPHp4fOlXGw8P", + "customer_creation": "if_required", + "customer_details": { + "address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "hook@me.com", + "name": "Martin", + "phone": null, + "tax_exempt": "none", + "tax_ids": [] + }, + "customer_email": null, + "expires_at": 1677920249, + "invoice": "in_1MhUT5E0b6fckueSfPlY05Fw", + "invoice_creation": null, + "livemode": false, + "locale": "auto", + "metadata": {}, + "mode": "subscription", + "payment_intent": null, + "payment_link": "plink_1MhUOwE0b6fckueSjmvf9Yea", + "payment_method_collection": "always", + "payment_method_options": {}, + "payment_method_types": [ + "card" + ], + "payment_status": "paid", + "phone_number_collection": { + "enabled": false + }, + "recovered_from": null, + "setup_intent": null, + "shipping_address_collection": null, + "shipping_options": [], + "status": "complete", + "submit_type": "auto", + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "success_url": "https://stripe.com", + "total_details": { + "amount_discount": 0, + "amount_shipping": 0, + "amount_tax": 0 + }, + "url": null, + "shipping_cost": null, + "shipping_details": null + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "checkout.session.completed" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "customer.created", + "name": "customer.created", + "body": { + "id": "evt_1MhUT6E0b6fckueSqWR0Bec4", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833998, + "data": { + "object": { + "id": "cus_NSPHp4fOlXGw8P", + "object": "customer", + "address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "balance": 0, + "created": 1677833998, + "currency": null, + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": "hook@me.com", + "invoice_prefix": "3DAA104C", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": {}, + "name": "Martin", + "next_invoice_sequence": 2, + "phone": null, + "preferred_locales": [ + "en-US" + ], + "shipping": null, + "tax_exempt": "none", + "test_clock": null + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "customer.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "customer.subscription.created", + "name": "customer.subscription.created", + "body": { + "id": "evt_1MhUZtE0b6fckueSDaOf1oJ8", + "object": "event", + "api_version": "2022-11-15", + "created": 1677834420, + "data": { + "object": { + "id": "sub_1MhUZrE0b6fckueSZfQ2Ytz6", + "object": "subscription", + "application": null, + "application_fee_percent": null, + "automatic_tax": { + "enabled": false + }, + "billing_cycle_anchor": 1680426419, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": null, + "cancellation_details": { + "comment": null, + "feedback": null, + "reason": null + }, + "collection_method": "charge_automatically", + "created": 1677834419, + "currency": "usd", + "current_period_end": 1680426419, + "current_period_start": 1677834419, + "customer": "cus_NSPOMxl5eL01qM", + "days_until_due": null, + "default_payment_method": "pm_1MhUZoE0b6fckueSNbTLQLkm", + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "ended_at": null, + "items": { + "object": "list", + "data": [ + { + "id": "si_NSPOg1R6mHTgyg", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1677834419, + "metadata": {}, + "plan": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 700, + "amount_decimal": "700", + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 700, + "unit_amount_decimal": "700" + }, + "quantity": 10, + "subscription": "sub_1MhUZrE0b6fckueSZfQ2Ytz6", + "tax_rates": [] + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_1MhUZrE0b6fckueSZfQ2Ytz6" + }, + "latest_invoice": "in_1MhUZrE0b6fckueSp9LCa3Rr", + "livemode": false, + "metadata": {}, + "next_pending_invoice_item_invoice": null, + "on_behalf_of": null, + "pause_collection": null, + "payment_settings": { + "payment_method_options": null, + "payment_method_types": null, + "save_default_payment_method": "off" + }, + "pending_invoice_item_interval": null, + "pending_setup_intent": null, + "pending_update": null, + "plan": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 700, + "amount_decimal": "700", + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 10, + "schedule": null, + "start_date": 1677834419, + "status": "trialing", + "test_clock": null, + "transfer_data": null, + "trial_end": 1680426419, + "trial_settings": { + "end_behavior": { + "missing_payment_method": "cancel" + } + }, + "trial_start": 1677834419 + } + }, + "livemode": false, + "pending_webhooks": 4, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "customer.subscription.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "customer.updated", + "name": "customer.updated", + "body": { + "id": "evt_1MhUT6E0b6fckueSfmuGgXWq", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833999, + "data": { + "object": { + "id": "cus_NSPHp4fOlXGw8P", + "object": "customer", + "address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "balance": 0, + "created": 1677833998, + "currency": "usd", + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": "hook@me.com", + "invoice_prefix": "3DAA104C", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": {}, + "name": "Martin", + "next_invoice_sequence": 2, + "phone": null, + "preferred_locales": [ + "en-US" + ], + "shipping": null, + "tax_exempt": "none", + "test_clock": null + }, + "previous_attributes": { + "currency": null + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "customer.updated" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "invoice.created", + "name": "invoice.created", + "body": { + "id": "evt_1MhUT7E0b6fckueStwy86nfu", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833999, + "data": { + "object": { + "id": "in_1MhUT5E0b6fckueSfPlY05Fw", + "object": "invoice", + "account_country": "US", + "account_name": "Rodriq", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "subscription_create", + "charge": null, + "collection_method": "charge_automatically", + "created": 1677833999, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NSPHp4fOlXGw8P", + "customer_address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "customer_email": "hook@me.com", + "customer_name": "Martin", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG?s=ap", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "object": "list", + "data": [ + { + "id": "il_1MhUT5E0b6fckueSJCaQVbIe", + "object": "line_item", + "amount": 0, + "amount_excluding_tax": 0, + "currency": "usd", + "description": "Trial period for Meetin Grande", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1680425999, + "start": 1677833999 + }, + "plan": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 700, + "amount_decimal": "700", + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 700, + "unit_amount_decimal": "700" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 11, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subscription_item": "si_NSPHIHu3GFH1sj", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription", + "unit_amount_excluding_tax": "0" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_1MhUT5E0b6fckueSfPlY05Fw/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3DAA104C-0001", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1677833999, + "period_start": 1677833999, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1677833999, + "marked_uncollectible_at": null, + "paid_at": 1677833999, + "voided_at": null + }, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null + } + }, + "livemode": false, + "pending_webhooks": 5, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "invoice.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "invoice.finalized", + "name": "invoice.finalized", + "body": { + "id": "evt_1MhUT7E0b6fckueS9G2IKvN4", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833999, + "data": { + "object": { + "id": "in_1MhUT5E0b6fckueSfPlY05Fw", + "object": "invoice", + "account_country": "US", + "account_name": "Rodriq", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "subscription_create", + "charge": null, + "collection_method": "charge_automatically", + "created": 1677833999, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NSPHp4fOlXGw8P", + "customer_address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "customer_email": "hook@me.com", + "customer_name": "Martin", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG?s=ap", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "object": "list", + "data": [ + { + "id": "il_1MhUT5E0b6fckueSJCaQVbIe", + "object": "line_item", + "amount": 0, + "amount_excluding_tax": 0, + "currency": "usd", + "description": "Trial period for Meetin Grande", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1680425999, + "start": 1677833999 + }, + "plan": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 700, + "amount_decimal": "700", + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 700, + "unit_amount_decimal": "700" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 11, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subscription_item": "si_NSPHIHu3GFH1sj", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription", + "unit_amount_excluding_tax": "0" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_1MhUT5E0b6fckueSfPlY05Fw/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3DAA104C-0001", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1677833999, + "period_start": 1677833999, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1677833999, + "marked_uncollectible_at": null, + "paid_at": 1677833999, + "voided_at": null + }, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null + } + }, + "livemode": false, + "pending_webhooks": 4, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "invoice.finalized" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "invoice.paid", + "name": "invoice.paid", + "body": { + "id": "evt_1MhUT7E0b6fckueShdwYrsWN", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833999, + "data": { + "object": { + "id": "in_1MhUT5E0b6fckueSfPlY05Fw", + "object": "invoice", + "account_country": "US", + "account_name": "Rodriq", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "subscription_create", + "charge": null, + "collection_method": "charge_automatically", + "created": 1677833999, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NSPHp4fOlXGw8P", + "customer_address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "customer_email": "hook@me.com", + "customer_name": "Martin", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG?s=ap", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "object": "list", + "data": [ + { + "id": "il_1MhUT5E0b6fckueSJCaQVbIe", + "object": "line_item", + "amount": 0, + "amount_excluding_tax": 0, + "currency": "usd", + "description": "Trial period for Meetin Grande", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1680425999, + "start": 1677833999 + }, + "plan": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 700, + "amount_decimal": "700", + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 700, + "unit_amount_decimal": "700" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 11, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subscription_item": "si_NSPHIHu3GFH1sj", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription", + "unit_amount_excluding_tax": "0" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_1MhUT5E0b6fckueSfPlY05Fw/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3DAA104C-0001", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1677833999, + "period_start": 1677833999, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1677833999, + "marked_uncollectible_at": null, + "paid_at": 1677833999, + "voided_at": null + }, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null + } + }, + "livemode": false, + "pending_webhooks": 4, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "invoice.paid" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "invoice.payment_succeeded", + "name": "invoice.payment_succeeded", + "body": { + "id": "evt_1MhUT7E0b6fckueSTtEJC9R4", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833999, + "data": { + "object": { + "id": "in_1MhUT5E0b6fckueSfPlY05Fw", + "object": "invoice", + "account_country": "US", + "account_name": "Rodriq", + "account_tax_ids": null, + "amount_due": 0, + "amount_paid": 0, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 0, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "status": null + }, + "billing_reason": "subscription_create", + "charge": null, + "collection_method": "charge_automatically", + "created": 1677833999, + "currency": "usd", + "custom_fields": null, + "customer": "cus_NSPHp4fOlXGw8P", + "customer_address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "customer_email": "hook@me.com", + "customer_name": "Martin", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG?s=ap", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1IvKcSE0b6fckueS/test_YWNjdF8xSXZLY1NFMGI2ZmNrdWVTLF9OU1BIM0pMOHVzalNXcGNINFpUM29ucVhrQm80M1F3LDY4Mzc0ODAx0200k1WbLvaG/pdf?s=ap", + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "object": "list", + "data": [ + { + "id": "il_1MhUT5E0b6fckueSJCaQVbIe", + "object": "line_item", + "amount": 0, + "amount_excluding_tax": 0, + "currency": "usd", + "description": "Trial period for Meetin Grande", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1680425999, + "start": 1677833999 + }, + "plan": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 700, + "amount_decimal": "700", + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1M3wjkE0b6fckueSKPgfRMSG", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1668409544, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_MnXpcVDdQq8wCY", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 700, + "unit_amount_decimal": "700" + }, + "proration": false, + "proration_details": { + "credited_items": null + }, + "quantity": 11, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subscription_item": "si_NSPHIHu3GFH1sj", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription", + "unit_amount_excluding_tax": "0" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_1MhUT5E0b6fckueSfPlY05Fw/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "3DAA104C-0001", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": null, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1677833999, + "period_start": 1677833999, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1677833999, + "marked_uncollectible_at": null, + "paid_at": 1677833999, + "voided_at": null + }, + "subscription": "sub_1MhUT5E0b6fckueSYAo1LoiF", + "subtotal": 0, + "subtotal_excluding_tax": 0, + "tax": null, + "test_clock": null, + "total": 0, + "total_discount_amounts": [], + "total_excluding_tax": 0, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null + } + }, + "livemode": false, + "pending_webhooks": 5, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "invoice.payment_succeeded" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "payment_link.created", + "name": "payment_link.created", + "body": { + "id": "evt_1MhUOwE0b6fckueScE3ilVdk", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833742, + "data": { + "object": { + "id": "plink_1MhUOwE0b6fckueSjmvf9Yea", + "object": "payment_link", + "active": true, + "after_completion": { + "hosted_confirmation": { + "custom_message": null + }, + "type": "hosted_confirmation" + }, + "allow_promotion_codes": false, + "application_fee_amount": null, + "application_fee_percent": null, + "automatic_tax": { + "enabled": false + }, + "billing_address_collection": "auto", + "consent_collection": { + "promotions": "none", + "terms_of_service": "none" + }, + "currency": "usd", + "custom_fields": [], + "custom_text": { + "shipping_address": null, + "submit": null + }, + "customer_creation": "if_required", + "invoice_creation": { + "enabled": false, + "invoice_data": { + "account_tax_ids": null, + "custom_fields": null, + "description": null, + "footer": null, + "metadata": {}, + "rendering_options": null + } + }, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "payment_intent_data": null, + "payment_method_collection": "always", + "payment_method_types": null, + "phone_number_collection": { + "enabled": false + }, + "shipping_address_collection": null, + "shipping_options": [], + "submit_type": "auto", + "subscription_data": { + "description": null, + "trial_period_days": 30 + }, + "tax_id_collection": { + "enabled": false + }, + "transfer_data": null, + "url": "https://buy.stripe.com/test_fZebKI0dz4Gf9Bm14h" + } + }, + "livemode": false, + "pending_webhooks": 10, + "request": { + "id": "req_yw1O7LbtRDYxgG", + "idempotency_key": "f6a8093e-63be-4656-a4a4-a08659ce719d" + }, + "type": "payment_link.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "payment_link.updated", + "name": "payment_link.updated", + "body": { + "id": "evt_1MhUpbE0b6fckueS04UJ4Hhr", + "object": "event", + "api_version": "2022-11-15", + "created": 1677835395, + "data": { + "object": { + "id": "plink_1MZqEkE0b6fckueSGoXNsufx", + "object": "payment_link", + "active": false, + "after_completion": { + "hosted_confirmation": { + "custom_message": null + }, + "type": "hosted_confirmation" + }, + "allow_promotion_codes": false, + "application_fee_amount": null, + "application_fee_percent": null, + "automatic_tax": { + "enabled": false + }, + "billing_address_collection": "auto", + "consent_collection": { + "promotions": "none", + "terms_of_service": "none" + }, + "currency": "usd", + "custom_fields": [], + "custom_text": { + "shipping_address": null, + "submit": null + }, + "customer_creation": "if_required", + "invoice_creation": { + "enabled": false, + "invoice_data": { + "account_tax_ids": null, + "custom_fields": null, + "description": null, + "footer": null, + "metadata": {}, + "rendering_options": null + } + }, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "payment_intent_data": null, + "payment_method_collection": "always", + "payment_method_types": null, + "phone_number_collection": { + "enabled": false + }, + "shipping_address_collection": null, + "shipping_options": [], + "submit_type": "auto", + "subscription_data": { + "description": null, + "trial_period_days": null + }, + "tax_id_collection": { + "enabled": false + }, + "transfer_data": null, + "url": "https://buy.stripe.com/test_4gw3ece4p1u3aFq4gr" + }, + "previous_attributes": { + "active": true + } + }, + "livemode": false, + "pending_webhooks": 8, + "request": { + "id": "req_dxXueNGdjQ9B47", + "idempotency_key": "5411e769-c2a9-4315-9965-ca16afded864" + }, + "type": "payment_link.updated" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "payment_method.attached", + "name": "payment_method.attached", + "body": { + "id": "evt_1MhUT6E0b6fckueSb5uPJGPA", + "object": "event", + "api_version": "2022-11-15", + "created": 1677833998, + "data": { + "object": { + "id": "pm_1MhUT1E0b6fckueSV6KgiBFn", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": "CM", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": "hook@me.com", + "name": "Martin", + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 12, + "exp_year": 2023, + "fingerprint": "iMCe31XWVrH1fujS", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1677833996, + "customer": "cus_NSPHp4fOlXGw8P", + "livemode": false, + "metadata": {}, + "type": "card" + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": null, + "idempotency_key": null + }, + "type": "payment_method.attached" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "setup_intent.created", + "name": "setup_intent.created", + "body": { + "id": "evt_1MhUZqE0b6fckueSDMcrg1zt", + "object": "event", + "api_version": "2022-11-15", + "created": 1677834417, + "data": { + "object": { + "id": "seti_1MhUZpE0b6fckueSO1ROP6RG", + "object": "setup_intent", + "application": null, + "cancellation_reason": null, + "client_secret": "seti_1MhUZpE0b6fckueSO1ROP6RG_secret_NSPOCUAQ6cVd3xDNuz1SvYqodrJrHqh", + "created": 1677834417, + "customer": null, + "description": null, + "flow_directions": null, + "last_setup_error": null, + "latest_attempt": "setatt_1MhUZpE0b6fckueSuPUf8BpQ", + "livemode": false, + "mandate": null, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "single_use_mandate": null, + "status": "succeeded", + "usage": "off_session" + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": "req_JFbK69aRBjemw9", + "idempotency_key": "58143ed3-e3a0-4a99-a612-8647a7d911b8" + }, + "type": "setup_intent.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "stripe", + "providerLabel": "Stripe", + "presetId": "stripe", + "eventType": "setup_intent.succeeded", + "name": "setup_intent.succeeded", + "body": { + "id": "evt_1MhUZqE0b6fckueSGwHOxNw6", + "object": "event", + "api_version": "2022-11-15", + "created": 1677834418, + "data": { + "object": { + "id": "seti_1MhUZpE0b6fckueSO1ROP6RG", + "object": "setup_intent", + "application": null, + "cancellation_reason": null, + "client_secret": "seti_1MhUZpE0b6fckueSO1ROP6RG_secret_NSPOCUAQ6cVd3xDNuz1SvYqodrJrHqh", + "created": 1677834417, + "customer": null, + "description": null, + "flow_directions": null, + "last_setup_error": null, + "latest_attempt": "setatt_1MhUZpE0b6fckueSuPUf8BpQ", + "livemode": false, + "mandate": null, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1MhUZoE0b6fckueSNbTLQLkm", + "payment_method_options": { + "card": { + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "single_use_mandate": null, + "status": "succeeded", + "usage": "off_session" + } + }, + "livemode": false, + "pending_webhooks": 3, + "request": { + "id": "req_JFbK69aRBjemw9", + "idempotency_key": "58143ed3-e3a0-4a99-a612-8647a7d911b8" + }, + "type": "setup_intent.succeeded" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:stripe/2022-11-15", + "snapshotDate": "2026-07" + } + }, + { + "provider": "woocommerce", + "providerLabel": "WooCommerce", + "eventType": "coupon.created", + "name": "coupon.created", + "body": { + "id": 17, + "code": "", + "amount": "10.00", + "status": "publish", + "date_created": "2023-03-25T16:22:37", + "date_created_gmt": "2023-03-25T16:22:37", + "date_modified": "2023-03-25T16:22:37", + "date_modified_gmt": "2023-03-25T16:22:37", + "discount_type": "percent", + "description": "", + "date_expires": "2023-03-31T00:00:00", + "date_expires_gmt": "2023-03-31T00:00:00", + "usage_count": 0, + "individual_use": false, + "product_ids": [], + "excluded_product_ids": [], + "usage_limit": null, + "usage_limit_per_user": null, + "limit_usage_to_x_items": null, + "free_shipping": false, + "product_categories": [], + "excluded_product_categories": [], + "exclude_sale_items": false, + "minimum_amount": "0.00", + "maximum_amount": "0.00", + "email_restrictions": [], + "used_by": [], + "meta_data": [], + "_links": { + "self": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/coupons/17" + } + ], + "collection": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/coupons" + } + ] + } + }, + "extraHeaders": { + "x-wc-webhook-event": "created", + "x-wc-webhook-resource": "coupon", + "x-wc-webhook-source": "http://localhost/wordpress/", + "x-wc-webhook-topic": "coupon.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:woocommerce/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "woocommerce", + "providerLabel": "WooCommerce", + "eventType": "coupon.deleted", + "name": "coupon.deleted", + "body": { + "id": 16 + }, + "extraHeaders": { + "x-wc-webhook-event": "deleted", + "x-wc-webhook-resource": "coupon", + "x-wc-webhook-source": "http://localhost/wordpress/", + "x-wc-webhook-topic": "coupon.deleted" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:woocommerce/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "woocommerce", + "providerLabel": "WooCommerce", + "eventType": "coupon.restored", + "name": "coupon.restored", + "body": { + "id": 16, + "code": "hook", + "amount": "5.00", + "status": "publish", + "date_created": "2023-03-25T16:04:36", + "date_created_gmt": "2023-03-25T16:04:36", + "date_modified": "2023-03-26T09:42:59", + "date_modified_gmt": "2023-03-26T09:42:59", + "discount_type": "percent", + "description": "", + "date_expires": "2023-03-31T00:00:00", + "date_expires_gmt": "2023-03-31T00:00:00", + "usage_count": 0, + "individual_use": false, + "product_ids": [], + "excluded_product_ids": [], + "usage_limit": null, + "usage_limit_per_user": null, + "limit_usage_to_x_items": null, + "free_shipping": false, + "product_categories": [], + "excluded_product_categories": [], + "exclude_sale_items": false, + "minimum_amount": "0.00", + "maximum_amount": "0.00", + "email_restrictions": [], + "used_by": [], + "meta_data": [ + { + "id": 77, + "key": "_wp_old_slug", + "value": "hook__trashed" + } + ], + "_links": { + "self": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/coupons/16" + } + ], + "collection": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/coupons" + } + ] + } + }, + "extraHeaders": { + "x-wc-webhook-event": "restored", + "x-wc-webhook-resource": "coupon", + "x-wc-webhook-source": "http://localhost/wordpress/", + "x-wc-webhook-topic": "coupon.restored" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:woocommerce/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "woocommerce", + "providerLabel": "WooCommerce", + "eventType": "product.created", + "name": "product.created", + "body": { + "id": 18, + "name": "Perpemint", + "slug": "perpemint", + "permalink": "http://localhost/wordpress/product/perpemint/", + "date_created": "2023-03-26T09:29:39", + "date_created_gmt": "2023-03-26T09:29:39", + "date_modified": "2023-03-26T09:29:39", + "date_modified_gmt": "2023-03-26T09:29:39", + "type": "simple", + "status": "publish", + "featured": false, + "catalog_visibility": "visible", + "description": "

Very Usefull

\n", + "short_description": "", + "sku": "", + "price": "150", + "regular_price": "200", + "sale_price": "150", + "date_on_sale_from": null, + "date_on_sale_from_gmt": null, + "date_on_sale_to": null, + "date_on_sale_to_gmt": null, + "on_sale": true, + "purchasable": true, + "total_sales": 0, + "virtual": false, + "downloadable": false, + "downloads": [], + "download_limit": -1, + "download_expiry": -1, + "external_url": "", + "button_text": "", + "tax_status": "taxable", + "tax_class": "", + "manage_stock": false, + "stock_quantity": null, + "backorders": "no", + "backorders_allowed": false, + "backordered": false, + "low_stock_amount": null, + "sold_individually": false, + "weight": "", + "dimensions": { + "length": "", + "width": "", + "height": "" + }, + "shipping_required": true, + "shipping_taxable": true, + "shipping_class": "", + "shipping_class_id": 0, + "reviews_allowed": true, + "average_rating": "0.00", + "rating_count": 0, + "upsell_ids": [], + "cross_sell_ids": [], + "parent_id": 0, + "purchase_note": "", + "categories": [ + { + "id": 15, + "name": "Uncategorized", + "slug": "uncategorized" + } + ], + "tags": [], + "images": [], + "attributes": [], + "default_attributes": [], + "variations": [], + "grouped_products": [], + "menu_order": 0, + "price_html": "200 CFA 150 CFA", + "related_ids": [ + 13, + 12 + ], + "meta_data": [], + "stock_status": "instock", + "has_options": false, + "_links": { + "self": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/products/18" + } + ], + "collection": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/products" + } + ] + } + }, + "extraHeaders": { + "x-wc-webhook-event": "created", + "x-wc-webhook-resource": "product", + "x-wc-webhook-source": "http://localhost/wordpress/", + "x-wc-webhook-topic": "product.created" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:woocommerce/current", + "snapshotDate": "2026-07" + } + }, + { + "provider": "woocommerce", + "providerLabel": "WooCommerce", + "eventType": "product.updated", + "name": "product.updated", + "body": { + "id": 18, + "name": "Perpemint", + "slug": "perpemint", + "permalink": "http://localhost/wordpress/product/perpemint/", + "date_created": "2023-03-26T09:29:39", + "date_created_gmt": "2023-03-26T09:29:39", + "date_modified": "2023-03-26T09:29:39", + "date_modified_gmt": "2023-03-26T09:29:39", + "type": "simple", + "status": "publish", + "featured": false, + "catalog_visibility": "visible", + "description": "

Very Usefull

\n", + "short_description": "", + "sku": "", + "price": "150", + "regular_price": "200", + "sale_price": "150", + "date_on_sale_from": null, + "date_on_sale_from_gmt": null, + "date_on_sale_to": null, + "date_on_sale_to_gmt": null, + "on_sale": true, + "purchasable": true, + "total_sales": 0, + "virtual": false, + "downloadable": false, + "downloads": [], + "download_limit": -1, + "download_expiry": -1, + "external_url": "", + "button_text": "", + "tax_status": "taxable", + "tax_class": "", + "manage_stock": false, + "stock_quantity": null, + "backorders": "no", + "backorders_allowed": false, + "backordered": false, + "low_stock_amount": null, + "sold_individually": false, + "weight": "", + "dimensions": { + "length": "", + "width": "", + "height": "" + }, + "shipping_required": true, + "shipping_taxable": true, + "shipping_class": "", + "shipping_class_id": 0, + "reviews_allowed": true, + "average_rating": "0.00", + "rating_count": 0, + "upsell_ids": [], + "cross_sell_ids": [], + "parent_id": 0, + "purchase_note": "", + "categories": [ + { + "id": 15, + "name": "Uncategorized", + "slug": "uncategorized" + } + ], + "tags": [], + "images": [], + "attributes": [], + "default_attributes": [], + "variations": [], + "grouped_products": [], + "menu_order": 0, + "price_html": "200 CFA 150 CFA", + "related_ids": [ + 12, + 13 + ], + "meta_data": [], + "stock_status": "instock", + "has_options": false, + "_links": { + "self": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/products/18" + } + ], + "collection": [ + { + "href": "http://localhost/wordpress/wp-json/wc/v3/products" + } + ] + } + }, + "extraHeaders": { + "x-wc-webhook-event": "updated", + "x-wc-webhook-resource": "product", + "x-wc-webhook-source": "http://localhost/wordpress/", + "x-wc-webhook-topic": "product.updated" + }, + "provenance": { + "kind": "upstream", + "ref": "hookdeck/webhook-samples:woocommerce/current", + "snapshotDate": "2026-07" + } + } +] diff --git a/internal-packages/webhook-sources/src/handAuthored/anthropic.ts b/internal-packages/webhook-sources/src/handAuthored/anthropic.ts new file mode 100644 index 00000000000..b67aa547494 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/anthropic.ts @@ -0,0 +1,119 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Anthropic (Managed Agents) webhook samples. Anthropic signs deliveries with a single + * `X-Webhook-Signature` header (secret `whsec_`-prefixed, verified via the SDK's + * `client.beta.webhooks.unwrap()`), not the three-header Standard Webhooks scheme our `svix` + * preset verifies, so these ship sample-only (no `presetId`). Every payload is a deliberately + * thin envelope: `{ type: "event", id, created_at, data: { type, id, organization_id, + * workspace_id } }`. Anthropic does not include the full resource in the delivery, by design + * ("avoids delivering stale data on retries and keeps every delivery small") — the integrator + * is expected to follow up with a `GET` against `data.id` (e.g. `sessions.retrieve(data.id)`). + */ +export const samples: SampleRecord[] = [ + { + provider: "anthropic", + providerLabel: "Anthropic (Claude)", + eventType: "session.status_run_started", + name: "Session run started", + description: + "A Managed Agents session transitioned to running. Fires on every transition into that status, not just the first. `data.id` is the session id; fetch it with `sessions.retrieve()` for the current state.", + body: { + type: "event", + id: "event_01HZXQ2K8VN3R5T7WA9BCEFGH", + created_at: "2026-07-14T09:12:03Z", + data: { + type: "session.status_run_started", + id: "sesn_01JX4KPQF7VN2R8TA9WBCDEFH", + organization_id: "8a3d2f1e-6b47-4c9a-9d2e-1f5c8a3b7e6d", + workspace_id: "c7b0e4d9-2a1f-4e6b-8c3d-5f9a7b2e4d1c", + }, + }, + docsUrl: "https://platform.claude.com/docs/en/managed-agents/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "anthropic", + providerLabel: "Anthropic (Claude)", + eventType: "session.status_idled", + name: "Session idled", + description: + "The agent is awaiting input — a tool permission approval or the next user message. `data.id` is the same session id from the run-started event; fetch it to read `stop_reason`.", + body: { + type: "event", + id: "event_01HZXQ3M9WP4S6U8XB1CDFGHJ", + created_at: "2026-07-14T09:14:47Z", + data: { + type: "session.status_idled", + id: "sesn_01JX4KPQF7VN2R8TA9WBCDEFH", + organization_id: "8a3d2f1e-6b47-4c9a-9d2e-1f5c8a3b7e6d", + workspace_id: "c7b0e4d9-2a1f-4e6b-8c3d-5f9a7b2e4d1c", + }, + }, + docsUrl: "https://platform.claude.com/docs/en/managed-agents/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "anthropic", + providerLabel: "Anthropic (Claude)", + eventType: "deployment_run.succeeded", + name: "Deployment run succeeded", + description: + "A scheduled deployment's cron firing created its session. `data.id` is the deployment run id (`drun_...`), not the session id — fetch the deployment run for its `session_id`, then subscribe to that session's events.", + body: { + type: "event", + id: "event_01HZXQ5R2YT6V9WA3CEGHJKLM", + created_at: "2026-07-14T20:00:01Z", + data: { + type: "deployment_run.succeeded", + id: "drun_01KM8NRTV2WY4Z6A9CDEFGHJ", + organization_id: "8a3d2f1e-6b47-4c9a-9d2e-1f5c8a3b7e6d", + workspace_id: "c7b0e4d9-2a1f-4e6b-8c3d-5f9a7b2e4d1c", + }, + }, + docsUrl: "https://platform.claude.com/docs/en/managed-agents/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "anthropic", + providerLabel: "Anthropic (Claude)", + eventType: "deployment_run.failed", + name: "Deployment run failed", + description: + "A scheduled deployment's cron firing did not create a session (e.g. the environment was archived, or the vault credential was missing). `data.id` is the deployment run id; fetch it for `error.type` / `error.message`.", + body: { + type: "event", + id: "event_01HZXQ6T4ZV8WB5CEGHJKLMNP", + created_at: "2026-07-15T02:00:02Z", + data: { + type: "deployment_run.failed", + id: "drun_01KM9PSUW3XZ5A7BDEFGHJKM", + organization_id: "8a3d2f1e-6b47-4c9a-9d2e-1f5c8a3b7e6d", + workspace_id: "c7b0e4d9-2a1f-4e6b-8c3d-5f9a7b2e4d1c", + }, + }, + docsUrl: "https://platform.claude.com/docs/en/managed-agents/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "anthropic", + providerLabel: "Anthropic (Claude)", + eventType: "agent.updated", + name: "Agent updated", + description: + "A new version of an agent config was published (updates that don't create a new version don't fire this). `data.id` is the agent id; fetch it for the new `version` and config.", + body: { + type: "event", + id: "event_01HZXQ7V6BW1XC7DFHJKLMNPQ", + created_at: "2026-07-15T11:30:19Z", + data: { + type: "agent.updated", + id: "agent_01LN2QTVX4YZ6B8CEFGHJKLN", + organization_id: "8a3d2f1e-6b47-4c9a-9d2e-1f5c8a3b7e6d", + workspace_id: "c7b0e4d9-2a1f-4e6b-8c3d-5f9a7b2e4d1c", + }, + }, + docsUrl: "https://platform.claude.com/docs/en/managed-agents/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/assemblyai.ts b/internal-packages/webhook-sources/src/handAuthored/assemblyai.ts new file mode 100644 index 00000000000..cb18155704b --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/assemblyai.ts @@ -0,0 +1,39 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * AssemblyAI transcript webhooks. The documented payload is intentionally minimal - just + * `transcript_id` and `status` - with only two possible `status` values (`completed`, `error`). Auth + * is a static shared secret header (`webhook_auth_header_name`/`webhook_auth_header_value`, set by the + * integrator when creating the transcript), not an HMAC/Ed25519 signature, so these ship sample-only + * (no `presetId`). + */ +export const samples: SampleRecord[] = [ + { + provider: "assemblyai", + providerLabel: "AssemblyAI", + eventType: "completed", + name: "Transcript completed", + description: + "The transcription finished successfully. Fetch GET /v2/transcript/{transcript_id} to retrieve the text.", + body: { + transcript_id: "5552493-16d8-42d8-8feb-c2a16b56f6e8", + status: "completed", + }, + docsUrl: "https://www.assemblyai.com/docs/concepts/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "assemblyai", + providerLabel: "AssemblyAI", + eventType: "error", + name: "Transcript error", + description: + "The transcription failed. Fetch GET /v2/transcript/{transcript_id} to read the `error` field with the failure reason.", + body: { + transcript_id: "8317204-9a2c-4f11-b6d0-1e6c4a8f7c3d", + status: "error", + }, + docsUrl: "https://www.assemblyai.com/docs/concepts/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/attio.ts b/internal-packages/webhook-sources/src/handAuthored/attio.ts new file mode 100644 index 00000000000..0ee92b1992b --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/attio.ts @@ -0,0 +1,145 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Attio samples. No preset: Attio signs with its own `attio-signature` header, a bare hex + * HMAC-SHA256 digest of the raw body (no `sha256=` prefix, no timestamp), which doesn't match + * stripe, github, svix, square, or discord. Each delivery wraps one event in a `webhook_id` + + * `events` envelope; Attio notes deliveries currently carry exactly one event but may batch more + * in future, so `events` is modeled as an array per the real shape. + */ +export const samples: SampleRecord[] = [ + { + provider: "attio", + providerLabel: "Attio", + eventType: "record.created", + name: "Record created (company)", + description: "A new company record was created in the workspace via the UI.", + body: { + webhook_id: "23e42eaf-323a-41da-b5bb-fd67eebda553", + events: [ + { + event_type: "record.created", + id: { + workspace_id: "14beef7a-99f7-4534-a87e-70b564330a4c", + object_id: "97052eb9-e65e-443f-a297-f2d9a4a7f795", + record_id: "bf071e1f-6035-429d-b874-d83ea64ea13b", + }, + actor: { + type: "workspace-member", + id: "50cf242c-7fa3-4cad-87d0-75b1af71c57b", + }, + }, + ], + }, + docsUrl: "https://docs.attio.com/rest-api/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "attio", + providerLabel: "Attio", + eventType: "record.updated", + name: "Record updated (stage attribute changed)", + description: "An existing record's deal-stage attribute was changed by a workspace member.", + body: { + webhook_id: "23e42eaf-323a-41da-b5bb-fd67eebda553", + events: [ + { + event_type: "record.updated", + id: { + workspace_id: "14beef7a-99f7-4534-a87e-70b564330a4c", + object_id: "6a1f3d9c-8b2e-4c7a-9d5f-2e8b1a4c7d9e", + record_id: "d4c8b2a6-1e9f-4d3c-8a7b-5f2e9c1d6a8b", + attribute_id: "41252299-f8c7-4b5e-99c9-4ff8321d2f96", + }, + actor: { + type: "workspace-member", + id: "50cf242c-7fa3-4cad-87d0-75b1af71c57b", + }, + }, + ], + }, + docsUrl: "https://docs.attio.com/rest-api/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "attio", + providerLabel: "Attio", + eventType: "record.deleted", + name: "Record deleted (person)", + description: "A person record was permanently deleted from the workspace.", + body: { + webhook_id: "23e42eaf-323a-41da-b5bb-fd67eebda553", + events: [ + { + event_type: "record.deleted", + id: { + workspace_id: "14beef7a-99f7-4534-a87e-70b564330a4c", + object_id: "9b3e6f1a-2c8d-4e5b-9f7a-1d4c8e2b6f9a", + record_id: "2f6b9d4e-8a1c-4f7d-9b3e-6c2a5f8d1b4c", + }, + actor: { + type: "workspace-member", + id: "7d2a9f4c-1b6e-4c8d-9a3f-5e1b8c2d6f9a", + }, + }, + ], + }, + docsUrl: "https://docs.attio.com/rest-api/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "attio", + providerLabel: "Attio", + eventType: "list-entry.created", + name: "List entry created (added to sales pipeline)", + description: "An existing company record was added as an entry to the sales pipeline list.", + body: { + webhook_id: "23e42eaf-323a-41da-b5bb-fd67eebda553", + events: [ + { + event_type: "list-entry.created", + id: { + workspace_id: "14beef7a-99f7-4534-a87e-70b564330a4c", + list_id: "33ebdbe9-e529-47c9-b894-0ba25e9c15c0", + entry_id: "2e6e29ea-c4e0-4f44-842d-78a891f8c156", + }, + parent_object_id: "97052eb9-e65e-443f-a297-f2d9a4a7f795", + parent_record_id: "bf071e1f-6035-429d-b874-d83ea64ea13b", + actor: { + type: "workspace-member", + id: "50cf242c-7fa3-4cad-87d0-75b1af71c57b", + }, + }, + ], + }, + docsUrl: "https://docs.attio.com/rest-api/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "attio", + providerLabel: "Attio", + eventType: "note.created", + name: "Note created (on a company record)", + description: "A workspace member added a note to an existing company record.", + body: { + webhook_id: "23e42eaf-323a-41da-b5bb-fd67eebda553", + events: [ + { + event_type: "note.created", + id: { + workspace_id: "14beef7a-99f7-4534-a87e-70b564330a4c", + note_id: "ff3f3bd4-40f4-4f80-8187-cd02385af424", + }, + parent_object_id: "97052eb9-e65e-443f-a297-f2d9a4a7f795", + parent_record_id: "bf071e1f-6035-429d-b874-d83ea64ea13b", + actor: { + type: "workspace-member", + id: "50cf242c-7fa3-4cad-87d0-75b1af71c57b", + }, + }, + ], + }, + docsUrl: "https://docs.attio.com/rest-api/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/auth0.ts b/internal-packages/webhook-sources/src/handAuthored/auth0.ts new file mode 100644 index 00000000000..7ad09c52cbe --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/auth0.ts @@ -0,0 +1,214 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Auth0 log stream samples. Auth0 authenticates these deliveries with a static bearer token (the + * Authorization Token set on the log stream), not an HMAC/Ed25519 signature, so no preset applies. + * Each delivery is a JSON array of log events; every element wraps the actual log fields under `data`, + * with `data.type` carrying the Auth0 log event type code (see the Log Event Type Codes reference). + */ +export const samples: SampleRecord[] = [ + { + provider: "auth0", + providerLabel: "Auth0", + eventType: "s", + name: "Successful login", + description: "A user successfully authenticated against a database connection.", + body: [ + { + log_id: "90020250708141238000000000000000001", + data: { + date: "2026-07-08T14:12:38.123Z", + type: "s", + description: "", + connection: "Username-Password-Authentication", + connection_id: "con_9f1KcxcOAJVzAKGh", + client_id: "AaiyAPdpYdesoKnqjj8HJqRn4T5titww", + client_name: "Trigger.dev Dashboard", + ip: "203.0.113.42", + user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + is_mobile: false, + hostname: "trigger-dev.us.auth0.com", + tenant_name: "trigger-dev", + environment_name: "production", + strategy: "auth0", + strategy_type: "database", + user_id: "auth0|64f1b2e3c9d4a10012345678", + user_name: "jordan@example.com", + log_id: "90020250708141238000000000000000001", + details: { + initiatedAt: 1752070357800, + completedAt: 1752070358123, + elapsedTime: 323, + session_id: "sess_2yQ8pXwK9nR3vT7hLmZ4jC1s", + }, + }, + }, + ], + docsUrl: "https://auth0.com/docs/customize/log-streams/custom-log-streams", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "auth0", + providerLabel: "Auth0", + eventType: "fp", + name: "Failed login (wrong password)", + description: "A login attempt failed because the password did not match the account.", + body: [ + { + log_id: "90020250708141902000000000000000002", + data: { + date: "2026-07-08T14:19:02.881Z", + type: "fp", + description: "Wrong password.", + connection: "Username-Password-Authentication", + connection_id: "con_9f1KcxcOAJVzAKGh", + client_id: "AaiyAPdpYdesoKnqjj8HJqRn4T5titww", + client_name: "Trigger.dev Dashboard", + ip: "198.51.100.17", + user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + is_mobile: false, + hostname: "trigger-dev.us.auth0.com", + tenant_name: "trigger-dev", + environment_name: "production", + strategy: "auth0", + strategy_type: "database", + user_id: "auth0|64f1b2e3c9d4a10012345678", + user_name: "jordan@example.com", + log_id: "90020250708141902000000000000000002", + details: { + error: { + message: "Wrong password.", + oauthError: "invalid_user_password", + type: "invalid_password", + }, + }, + }, + }, + ], + docsUrl: "https://auth0.com/docs/customize/log-streams/custom-log-streams", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "auth0", + providerLabel: "Auth0", + eventType: "fu", + name: "Failed login (invalid username)", + description: "A login attempt failed because no account matched the given identifier.", + body: [ + { + log_id: "90020250708142511000000000000000003", + data: { + date: "2026-07-08T14:25:11.402Z", + type: "fu", + description: "Invalid username or password.", + connection: "Username-Password-Authentication", + connection_id: "con_9f1KcxcOAJVzAKGh", + client_id: "AaiyAPdpYdesoKnqjj8HJqRn4T5titww", + client_name: "Trigger.dev Dashboard", + ip: "203.0.113.90", + user_agent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15", + is_mobile: true, + hostname: "trigger-dev.us.auth0.com", + tenant_name: "trigger-dev", + environment_name: "production", + strategy: "auth0", + strategy_type: "database", + user_id: null, + user_name: "unknown-visitor@example.com", + log_id: "90020250708142511000000000000000003", + details: { + error: { + message: "Invalid username or password.", + oauthError: "invalid_user_password", + type: "invalid_username", + }, + }, + }, + }, + ], + docsUrl: "https://auth0.com/docs/customize/log-streams/custom-log-streams", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "auth0", + providerLabel: "Auth0", + eventType: "f", + name: "Failed login (federated connection denied)", + description: + "A login attempt failed for a reason outside of bad credentials, e.g. a Post-Login Action denying access on a social connection.", + body: [ + { + log_id: "90020250708150044000000000000000004", + data: { + date: "2026-07-08T15:00:44.219Z", + type: "f", + description: "Access denied.", + connection: "google-oauth2", + connection_id: "con_7bWmQdRk2LpNxVaU", + client_id: "AaiyAPdpYdesoKnqjj8HJqRn4T5titww", + client_name: "Trigger.dev Dashboard", + ip: "203.0.113.211", + user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", + is_mobile: false, + hostname: "trigger-dev.us.auth0.com", + tenant_name: "trigger-dev", + environment_name: "production", + strategy: "google-oauth2", + strategy_type: "social", + user_id: "google-oauth2|108234567890123456789", + user_name: "morgan@example.com", + log_id: "90020250708150044000000000000000004", + details: { + error: { + message: "Access denied.", + oauthError: "access_denied", + type: "oauth-authorization-code", + }, + }, + }, + }, + ], + docsUrl: "https://auth0.com/docs/customize/log-streams/custom-log-streams", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "auth0", + providerLabel: "Auth0", + eventType: "ss", + name: "Successful signup", + description: "A new user completed signup against a database connection.", + body: [ + { + log_id: "90020250708153327000000000000000005", + data: { + date: "2026-07-08T15:33:27.556Z", + type: "ss", + description: "", + connection: "Username-Password-Authentication", + connection_id: "con_9f1KcxcOAJVzAKGh", + client_id: "AaiyAPdpYdesoKnqjj8HJqRn4T5titww", + client_name: "Trigger.dev Dashboard", + ip: "203.0.113.55", + user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + is_mobile: false, + hostname: "trigger-dev.us.auth0.com", + tenant_name: "trigger-dev", + environment_name: "production", + strategy: "auth0", + strategy_type: "database", + user_id: "auth0|64f1c9a7d8e2b30012349999", + user_name: "taylor@example.com", + log_id: "90020250708153327000000000000000005", + details: { + initiatedAt: 1752075206900, + completedAt: 1752075207556, + elapsedTime: 656, + session_id: "sess_8kR2wLpN4vT9jC1hZmQ3yX7s", + }, + }, + }, + ], + docsUrl: "https://auth0.com/docs/customize/log-streams/custom-log-streams", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/brex.ts b/internal-packages/webhook-sources/src/handAuthored/brex.ts new file mode 100644 index 00000000000..c96bd2f69ce --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/brex.ts @@ -0,0 +1,95 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Brex samples. Confirmed shapes: `TRANSFER_PROCESSED`/`TRANSFER_FAILED` (docs' own verified test + * fixture: `event_type`, `transfer_id`, `company_id`) and `EXPENSE_PAYMENT_UPDATED` (docs' webhook + * examples page, a card-charge event: `event_type`, `expense_id`, `payment_status`, `payment_type`, + * `company_id`, `amount`, `payment_description`). `REFERRAL_ACTIVATED` and `USER_UPDATED` are listed + * in the docs' webhook event catalog table but shown there without a sample payload, so their bodies + * extrapolate the same minimal `event_type` + resource id + `company_id` shape the confirmed events + * use. + */ +export const samples: SampleRecord[] = [ + { + provider: "brex", + providerLabel: "Brex", + presetId: "svix", + eventType: "TRANSFER_PROCESSED", + name: "Transfer processed", + description: "A transfer was successfully received.", + body: { + event_type: "TRANSFER_PROCESSED", + transfer_id: "dptx_ck9g2h4nq000101ms2s5cz3rq", + company_id: "cuacc_ck9wodfq7h000801q48qqsae5k", + }, + docsUrl: "https://developer.brex.com/docs/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "brex", + providerLabel: "Brex", + presetId: "svix", + eventType: "TRANSFER_FAILED", + name: "Transfer failed", + description: "A transfer attempt failed.", + body: { + event_type: "TRANSFER_FAILED", + transfer_id: "dptx_ck3h9k2mp000201nt3t6da4sw", + company_id: "cuacc_ck9wodfq7h000801q48qqsae5k", + }, + docsUrl: "https://developer.brex.com/docs/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "brex", + providerLabel: "Brex", + presetId: "svix", + eventType: "EXPENSE_PAYMENT_UPDATED", + name: "Card expense payment updated", + description: "A charge on a Brex card produced a new or updated expense payment.", + body: { + event_type: "EXPENSE_PAYMENT_UPDATED", + expense_id: "expense_cl3khzfnr00000k92bdjtmmes", + payment_status: "CLEARED", + payment_type: "PURCHASE", + company_id: "cuacc_ck9wodfq7h000801q48qqsae5k", + amount: { + amount: 4899, + currency: "USD", + }, + payment_description: "Acme Cloud Hosting", + }, + docsUrl: "https://developer.brex.com/docs/webhook_examples/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "brex", + providerLabel: "Brex", + presetId: "svix", + eventType: "REFERRAL_ACTIVATED", + name: "Referral activated", + description: "A referred contact signed up for Brex using a referral link.", + body: { + event_type: "REFERRAL_ACTIVATED", + referral_id: "referral_ck7m3n9pq000401vb8x2ez6ta", + company_id: "cuacc_ck9wodfq7h000801q48qqsae5k", + }, + docsUrl: "https://developer.brex.com/docs/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "brex", + providerLabel: "Brex", + presetId: "svix", + eventType: "USER_UPDATED", + name: "User updated", + description: "A user on the account was updated.", + body: { + event_type: "USER_UPDATED", + user_id: "buser_ck2p6r4st000501wc9y3fa7ub", + company_id: "cuacc_ck9wodfq7h000801q48qqsae5k", + }, + docsUrl: "https://developer.brex.com/docs/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/cal-com.ts b/internal-packages/webhook-sources/src/handAuthored/cal-com.ts new file mode 100644 index 00000000000..eb0332f4106 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/cal-com.ts @@ -0,0 +1,163 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Cal.com samples. No preset: Cal.com signs with its own `x-cal-signature-256` header, a plain hex + * HMAC-SHA256 digest of the raw body (no `sha256=` prefix), which does not match our `github` + * preset's wire format. `MEETING_ENDED` is an exception among these events: its fields sit flat at + * the top level instead of nested under `payload`. + */ +export const samples: SampleRecord[] = [ + { + provider: "cal-com", + providerLabel: "Cal.com", + eventType: "BOOKING_CREATED", + name: "Booking created", + description: "A new booking was made for an event type and accepted immediately.", + body: { + triggerEvent: "BOOKING_CREATED", + createdAt: "2026-07-01T14:05:00.000Z", + payload: { + bookerUrl: "https://cal.com/jordan-lee", + title: "30 Min Meeting between Jordan Lee and Priya Nair", + startTime: "2026-07-03T16:00:00.000Z", + endTime: "2026-07-03T16:30:00.000Z", + eventTypeId: 48213, + organizer: { + id: 9021, + name: "Jordan Lee", + email: "jordan.lee@example.com", + username: "jordan-lee", + timeZone: "America/Los_Angeles", + timeFormat: "h:mma", + }, + attendees: [ + { + email: "priya.nair@example.com", + name: "Priya Nair", + timeZone: "Asia/Kolkata", + }, + ], + location: "https://meet.google.com/abc-defg-hij", + uid: "8f2b6c1a9d3e4a7fb5c21e6f9a0d4b7c", + bookingId: 5502391, + status: "ACCEPTED", + }, + }, + docsUrl: "https://cal.com/docs/developing/guides/automation/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "cal-com", + providerLabel: "Cal.com", + eventType: "BOOKING_CANCELLED", + name: "Booking cancelled", + description: "The attendee cancelled an existing booking and gave a reason.", + body: { + triggerEvent: "BOOKING_CANCELLED", + createdAt: "2026-07-02T09:41:12.000Z", + payload: { + title: "30 Min Meeting between Jordan Lee and Priya Nair", + startTime: "2026-07-03T16:00:00.000Z", + endTime: "2026-07-03T16:30:00.000Z", + organizer: { + id: 9021, + name: "Jordan Lee", + email: "jordan.lee@example.com", + username: "jordan-lee", + timeZone: "America/Los_Angeles", + timeFormat: "h:mma", + }, + attendees: [ + { + email: "priya.nair@example.com", + name: "Priya Nair", + timeZone: "Asia/Kolkata", + }, + ], + uid: "8f2b6c1a9d3e4a7fb5c21e6f9a0d4b7c", + bookingId: 5502391, + cancellationReason: "Something came up, need to reschedule for next week.", + status: "CANCELLED", + }, + }, + docsUrl: "https://cal.com/docs/developing/guides/automation/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "cal-com", + providerLabel: "Cal.com", + eventType: "BOOKING_RESCHEDULED", + name: "Booking rescheduled", + description: + "An existing booking was moved to a new time; the payload links old and new slots.", + body: { + triggerEvent: "BOOKING_RESCHEDULED", + createdAt: "2026-07-04T11:18:47.000Z", + payload: { + title: "30 Min Meeting between Jordan Lee and Priya Nair", + startTime: "2026-07-03T16:00:00.000Z", + endTime: "2026-07-03T16:30:00.000Z", + organizer: { + id: 9021, + name: "Jordan Lee", + email: "jordan.lee@example.com", + username: "jordan-lee", + timeZone: "America/Los_Angeles", + timeFormat: "h:mma", + }, + attendees: [ + { + email: "priya.nair@example.com", + name: "Priya Nair", + timeZone: "Asia/Kolkata", + }, + ], + uid: "8f2b6c1a9d3e4a7fb5c21e6f9a0d4b7c", + bookingId: 5502391, + rescheduleId: 5502391, + rescheduleUid: "3f6a1c2e71b44b8d9e2a6d4f0a8b1c3d", + rescheduleStartTime: "2026-07-10T17:00:00.000Z", + rescheduleEndTime: "2026-07-10T17:30:00.000Z", + status: "ACCEPTED", + }, + }, + docsUrl: "https://cal.com/docs/developing/guides/automation/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "cal-com", + providerLabel: "Cal.com", + eventType: "MEETING_ENDED", + name: "Meeting ended", + description: + "A booked meeting finished. Unlike the other trigger events, MEETING_ENDED carries its fields flat at the top level instead of under payload.", + body: { + triggerEvent: "MEETING_ENDED", + id: 5502391, + uid: "8f2b6c1a9d3e4a7fb5c21e6f9a0d4b7c", + userId: 9021, + eventTypeId: 48213, + title: "30 Min Meeting between Jordan Lee and Priya Nair", + startTime: "2026-07-03T16:00:00.000Z", + endTime: "2026-07-03T16:30:00.000Z", + createdAt: "2026-07-01T14:05:00.000Z", + updatedAt: "2026-07-03T16:30:05.000Z", + status: "ACCEPTED", + user: { + email: "jordan.lee@example.com", + name: "Jordan Lee", + timeZone: "America/Los_Angeles", + }, + attendees: [ + { + id: 771034, + email: "priya.nair@example.com", + name: "Priya Nair", + timeZone: "Asia/Kolkata", + }, + ], + }, + docsUrl: "https://cal.com/docs/developing/guides/automation/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/calendly.ts b/internal-packages/webhook-sources/src/handAuthored/calendly.ts new file mode 100644 index 00000000000..95a72577b1f --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/calendly.ts @@ -0,0 +1,236 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Calendly samples. No preset: Calendly signs with its own `Calendly-Webhook-Signature` header + * (`t=,v1=`, HMAC-SHA256 over `{t}.{rawBody}`), which does not match `stripe-signature`. + */ +export const samples: SampleRecord[] = [ + { + provider: "calendly", + providerLabel: "Calendly", + eventType: "invitee.created", + name: "Invitee created (one-on-one)", + description: "A new invitee booked a one-on-one event, with a custom question answered.", + body: { + created_at: "2026-06-01T15:22:41.000000Z", + event: "invitee.created", + payload: { + cancel_url: "https://calendly.com/cancellations/3f6a1c2e-71b4-4b8d-9e2a-6d4f0a8b1c3d", + created_at: "2026-06-01T15:22:41.000000Z", + email: "jordan.rivera@example.com", + event: "https://api.calendly.com/scheduled_events/8f2b6c1a-9d3e-4a7f-b5c2-1e6f9a0d4b7c", + name: "Jordan Rivera", + new_invitee: null, + old_invitee: null, + questions_and_answers: [ + { + question: "What would you like to discuss?", + answer: "Onboarding walkthrough for the new workspace.", + position: 0, + }, + ], + reschedule_url: "https://calendly.com/reschedulings/3f6a1c2e-71b4-4b8d-9e2a-6d4f0a8b1c3d", + rescheduled: false, + status: "active", + text_reminder_number: null, + timezone: "America/Los_Angeles", + tracking: { + utm_campaign: null, + utm_source: null, + utm_medium: null, + utm_content: null, + utm_term: null, + salesforce_uuid: null, + }, + updated_at: "2026-06-01T15:22:41.000000Z", + uri: "https://api.calendly.com/scheduled_events/8f2b6c1a-9d3e-4a7f-b5c2-1e6f9a0d4b7c/invitees/1a4d7e2b-5c8f-4a1d-9b3e-6f0c8d2a5b7e", + }, + }, + docsUrl: "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "calendly", + providerLabel: "Calendly", + eventType: "invitee.created", + name: "Invitee created (group event, via campaign link)", + description: "A new invitee joined a group event through a link carrying UTM tracking params.", + body: { + created_at: "2026-06-03T09:05:12.000000Z", + event: "invitee.created", + payload: { + cancel_url: "https://calendly.com/cancellations/9a1e5f3c-2b7d-4c6a-8e1f-0d3b6a9c2e5f", + created_at: "2026-06-03T09:05:12.000000Z", + email: "priya.nair@example.com", + event: "https://api.calendly.com/scheduled_events/2c5e8a1d-4f7b-4d9e-a2c5-8b1e4f7a0d3c", + name: "Priya Nair", + new_invitee: null, + old_invitee: null, + questions_and_answers: [], + reschedule_url: "https://calendly.com/reschedulings/9a1e5f3c-2b7d-4c6a-8e1f-0d3b6a9c2e5f", + rescheduled: false, + status: "active", + text_reminder_number: null, + timezone: "Europe/London", + tracking: { + utm_campaign: "summer-webinar", + utm_source: "newsletter", + utm_medium: "email", + utm_content: "cta-button", + utm_term: null, + salesforce_uuid: null, + }, + updated_at: "2026-06-03T09:05:12.000000Z", + uri: "https://api.calendly.com/scheduled_events/2c5e8a1d-4f7b-4d9e-a2c5-8b1e4f7a0d3c/invitees/6d9c2a5e-7f1b-4a3d-9e6c-2b5f8a1d4c7e", + }, + }, + docsUrl: "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "calendly", + providerLabel: "Calendly", + eventType: "invitee.created", + name: "Invitee created (rebooked after reschedule)", + description: + "The replacement invitee record created when an existing invitee reschedules; old_invitee links back to the canceled booking.", + body: { + created_at: "2026-06-05T18:41:07.000000Z", + event: "invitee.created", + payload: { + cancel_url: "https://calendly.com/cancellations/5b8e1c4a-9f2d-4b6e-8c1a-3d5f9b2e6a8c", + created_at: "2026-06-05T18:41:07.000000Z", + email: "morgan.blake@example.com", + event: "https://api.calendly.com/scheduled_events/4d7f0a3c-6b9e-4c2d-8f5a-1e3d6b9c2f5a", + name: "Morgan Blake", + new_invitee: null, + old_invitee: + "https://api.calendly.com/scheduled_events/9e2c5a8d-1f4b-4d7e-a3c6-5b8e1a4d7c9e/invitees/3c6f9a2d-5e8b-4a1c-9d3f-6a9c2e5b8d1f", + questions_and_answers: [ + { + question: "Anything else we should know?", + answer: "Moving the call an hour later.", + position: 0, + }, + ], + reschedule_url: "https://calendly.com/reschedulings/5b8e1c4a-9f2d-4b6e-8c1a-3d5f9b2e6a8c", + rescheduled: false, + status: "active", + text_reminder_number: null, + timezone: "America/New_York", + tracking: { + utm_campaign: null, + utm_source: null, + utm_medium: null, + utm_content: null, + utm_term: null, + salesforce_uuid: null, + }, + updated_at: "2026-06-05T18:41:07.000000Z", + uri: "https://api.calendly.com/scheduled_events/4d7f0a3c-6b9e-4c2d-8f5a-1e3d6b9c2f5a/invitees/8f1a4d7c-2e5b-4a9d-8c1f-3e6a9d2c5f8b", + }, + }, + docsUrl: "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "calendly", + providerLabel: "Calendly", + eventType: "invitee.canceled", + name: "Invitee canceled (self-canceled with reason)", + description: "The invitee canceled their own booking and left a cancellation reason.", + body: { + created_at: "2026-06-08T11:30:55.000000Z", + event: "invitee.canceled", + payload: { + cancel_url: "https://calendly.com/cancellations/3f6a1c2e-71b4-4b8d-9e2a-6d4f0a8b1c3d", + cancellation: { + canceled_by: "Jordan Rivera", + canceler_type: "invitee", + reason: "Something came up, will rebook next week.", + }, + created_at: "2026-06-01T15:22:41.000000Z", + email: "jordan.rivera@example.com", + event: "https://api.calendly.com/scheduled_events/8f2b6c1a-9d3e-4a7f-b5c2-1e6f9a0d4b7c", + name: "Jordan Rivera", + new_invitee: null, + old_invitee: null, + questions_and_answers: [ + { + question: "What would you like to discuss?", + answer: "Onboarding walkthrough for the new workspace.", + position: 0, + }, + ], + reschedule_url: "https://calendly.com/reschedulings/3f6a1c2e-71b4-4b8d-9e2a-6d4f0a8b1c3d", + rescheduled: false, + status: "canceled", + text_reminder_number: null, + timezone: "America/Los_Angeles", + tracking: { + utm_campaign: null, + utm_source: null, + utm_medium: null, + utm_content: null, + utm_term: null, + salesforce_uuid: null, + }, + updated_at: "2026-06-08T11:30:55.000000Z", + uri: "https://api.calendly.com/scheduled_events/8f2b6c1a-9d3e-4a7f-b5c2-1e6f9a0d4b7c/invitees/1a4d7e2b-5c8f-4a1d-9b3e-6f0c8d2a5b7e", + }, + }, + docsUrl: "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "calendly", + providerLabel: "Calendly", + eventType: "invitee.canceled", + name: "Invitee canceled (host rescheduled the meeting)", + description: + "The host rescheduled the meeting on the invitee's behalf; rescheduled is true and new_invitee points at the replacement booking.", + body: { + created_at: "2026-06-05T18:41:07.000000Z", + event: "invitee.canceled", + payload: { + cancel_url: "https://calendly.com/cancellations/9e2c5a8d-1f4b-4d7e-a3c6-5b8e1a4d7c9e", + cancellation: { + canceled_by: "Alex Chen", + canceler_type: "host", + reason: null, + }, + created_at: "2026-06-02T13:00:00.000000Z", + email: "morgan.blake@example.com", + event: "https://api.calendly.com/scheduled_events/9e2c5a8d-1f4b-4d7e-a3c6-5b8e1a4d7c9e", + name: "Morgan Blake", + new_invitee: + "https://api.calendly.com/scheduled_events/4d7f0a3c-6b9e-4c2d-8f5a-1e3d6b9c2f5a/invitees/8f1a4d7c-2e5b-4a9d-8c1f-3e6a9d2c5f8b", + old_invitee: null, + questions_and_answers: [ + { + question: "Anything else we should know?", + answer: "Moving the call an hour later.", + position: 0, + }, + ], + reschedule_url: "https://calendly.com/reschedulings/9e2c5a8d-1f4b-4d7e-a3c6-5b8e1a4d7c9e", + rescheduled: true, + status: "canceled", + text_reminder_number: null, + timezone: "America/New_York", + tracking: { + utm_campaign: null, + utm_source: null, + utm_medium: null, + utm_content: null, + utm_term: null, + salesforce_uuid: null, + }, + updated_at: "2026-06-05T18:41:07.000000Z", + uri: "https://api.calendly.com/scheduled_events/9e2c5a8d-1f4b-4d7e-a3c6-5b8e1a4d7c9e/invitees/3c6f9a2d-5e8b-4a1c-9d3f-6a9c2e5b8d1f", + }, + }, + docsUrl: "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/clerk.ts b/internal-packages/webhook-sources/src/handAuthored/clerk.ts new file mode 100644 index 00000000000..791bc65bd50 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/clerk.ts @@ -0,0 +1,39 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Clerk samples. Clerk signs webhooks with Svix, so `presetId: "svix"` keeps them under the round-trip + * guarantee while carrying their own freeform `provider`/`providerLabel`. + */ +export const samples: SampleRecord[] = [ + { + provider: "clerk", + providerLabel: "Clerk", + presetId: "svix", + eventType: "user.created", + name: "User created", + description: "A new Clerk user. Clerk signs webhooks with Svix.", + body: { + type: "user.created", + object: "event", + data: { + id: "user_29w83sxmDNGwOuEthce5gg56FcC", + object: "user", + first_name: "Jordan", + last_name: "Lee", + email_addresses: [ + { + id: "idn_29w83yL7CwVlJXylYLxcslromF1", + object: "email_address", + email_address: "jordan@example.com", + verification: { status: "verified", strategy: "email_code" }, + }, + ], + primary_email_address_id: "idn_29w83yL7CwVlJXylYLxcslromF1", + created_at: 1654012591514, + updated_at: 1654012591835, + }, + }, + docsUrl: "https://clerk.com/docs/integrations/webhooks/overview", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/close-crm.ts b/internal-packages/webhook-sources/src/handAuthored/close-crm.ts new file mode 100644 index 00000000000..ac4a2ca9efd --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/close-crm.ts @@ -0,0 +1,287 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Close samples. Every delivery wraps a single Event Log entry in `{ event, subscription_id }`; + * `event.object_type` + `event.action` together identify the resource and action (Close's own docs + * list them as separate fields, e.g. `lead` / `created`), so `eventType` here joins them as + * `"."`. Close does not sign with any of our verifier presets (see the registry + * entry), so no sample sets `presetId`. The `lead.created` and `activity.*.created` bodies follow the + * Lead / Contact / CallActivity field names from the Close API reference; the opportunity-status-change + * activity's `old_status` / `new_status` fields are inferred from the Event Log docs' description of + * that activity type (the reference page for that resource ships no field-level schema). The final + * sample is Close's own published webhook example (an opportunity moving to "Won"), lightly + * re-dated. + */ +export const samples: SampleRecord[] = [ + { + provider: "close-crm", + providerLabel: "Close", + eventType: "lead.created", + name: "Lead created", + description: "A new lead was created in the organization, regardless of the method used.", + body: { + event: { + id: "ev_2r8KzQyF7bWmNdE1TxJcP4", + date_created: "2026-07-10T15:02:11.204000", + date_updated: "2026-07-10T15:02:11.204000", + object_type: "lead", + object_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + user_id: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + request_id: "req_7YtRj2AmVp3Km9RtWzY6Qa", + action: "created", + changed_fields: [], + previous_data: null, + data: { + id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + name: "Acme Rocket Co", + display_name: "Acme Rocket Co", + description: "Inbound demo request from the pricing page.", + url: "https://acmerocket.example", + status_id: "stat_1Nc4Bf7Hn4Wq7VtRj2AmY8", + status_label: "Potential", + html_url: "https://app.close.com/lead/lead_9k2R7wYtVbXmS3Ac6Fq1Zp/", + addresses: [ + { + label: "business", + address_1: "142 Harbor Ave", + address_2: "Suite 400", + city: "Portland", + state: "OR", + zipcode: "97201", + country: "US", + }, + ], + contacts: [ + { + id: "cont_Bf7Hn4Wq7VtRj2AmY8bKzX", + name: "Dana Whitfield", + title: "VP Operations", + display_name: "Dana Whitfield", + emails: [ + { email: "dana@acmerocket.example", type: "office", is_unsubscribed: false }, + ], + phones: [ + { + phone: "+15035550142", + type: "office", + phone_formatted: "+1 503-555-0142", + country: "US", + }, + ], + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + }, + ], + custom: {}, + created_by: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + created_by_name: "Sam Okafor", + updated_by: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + date_created: "2026-07-10T15:02:11.198000+00:00", + date_updated: "2026-07-10T15:02:11.198000+00:00", + }, + meta: { request_method: "POST", request_path: "/api/v1/lead/" }, + }, + subscription_id: "whsub_Km9RtWzY6QaX1Nc4Bf7Hn4", + }, + docsUrl: "https://developer.close.com/topics/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "close-crm", + providerLabel: "Close", + eventType: "activity.note.created", + name: "Note activity created", + description: "A freeform note was logged on a lead.", + body: { + event: { + id: "ev_4TxJcP4r8KzQyF7bWmNdE1", + date_created: "2026-07-10T15:11:47.512000", + date_updated: "2026-07-10T15:11:47.512000", + object_type: "activity.note", + object_id: "acti_Wq7VtRj2AmY8bKzXc1SHn4", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + user_id: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + request_id: "req_9RtWzY6QaX1Nc4Bf7Hn4Wq", + action: "created", + changed_fields: [], + previous_data: null, + data: { + id: "acti_Wq7VtRj2AmY8bKzXc1SHn4", + _type: "Note", + note: "Left a voicemail, asked to follow up next week after their board meeting.", + note_html: + "

Left a voicemail, asked to follow up next week after their board meeting.

", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + contact_id: "cont_Bf7Hn4Wq7VtRj2AmY8bKzX", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + created_by: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + created_by_name: "Sam Okafor", + updated_by: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + date_created: "2026-07-10T15:11:47.502000+00:00", + date_updated: "2026-07-10T15:11:47.502000+00:00", + }, + meta: { request_method: "POST", request_path: "/api/v1/activity/note/" }, + }, + subscription_id: "whsub_Km9RtWzY6QaX1Nc4Bf7Hn4", + }, + docsUrl: "https://developer.close.com/api/resources/activities/notes.md", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "close-crm", + providerLabel: "Close", + eventType: "activity.call.created", + name: "Call activity created", + description: "An inbound call was logged on a lead's contact.", + body: { + event: { + id: "ev_7bWmNdE1TxJcP4r8KzQyF4", + date_created: "2026-07-10T16:04:33.877000", + date_updated: "2026-07-10T16:04:33.877000", + object_type: "activity.call", + object_id: "acti_c1SHn4Wq7VtRj2AmY8bKzX", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + user_id: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + request_id: "req_1Nc4Bf7Hn4Wq7VtRj2AmY8", + action: "created", + changed_fields: [], + previous_data: null, + data: { + id: "acti_c1SHn4Wq7VtRj2AmY8bKzX", + _type: "Call", + direction: "inbound", + status: "completed", + duration: 184, + phone: "+15035550142", + note: "", + recording_url: null, + outcome_id: null, + contact_id: "cont_Bf7Hn4Wq7VtRj2AmY8bKzX", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + created_by: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + created_by_name: "Sam Okafor", + date_created: "2026-07-10T16:04:33.861000+00:00", + date_updated: "2026-07-10T16:04:33.861000+00:00", + }, + meta: { request_method: "POST", request_path: "/api/v1/activity/call/" }, + }, + subscription_id: "whsub_Km9RtWzY6QaX1Nc4Bf7Hn4", + }, + docsUrl: "https://developer.close.com/api/resources/activities/calls.md", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "close-crm", + providerLabel: "Close", + eventType: "activity.opportunity_status_change.created", + name: "Opportunity status change activity created", + description: + "A dedicated status-change activity was created for an opportunity (distinct from the opportunity's own `updated` event); `new_status`/`old_status` name the transition.", + body: { + event: { + id: "ev_Bf7Hn4Wq7VtRj2AmY8bKzX", + date_created: "2026-07-10T17:20:05.339000", + date_updated: "2026-07-10T17:20:05.339000", + object_type: "activity.opportunity_status_change", + object_id: "acti_5R3RfhIQixdnmjf2wMS9M6", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + user_id: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + request_id: "req_c1SHn4Wq7VtRj2AmY8bKzXc", + action: "created", + changed_fields: [], + previous_data: null, + data: { + id: "acti_5R3RfhIQixdnmjf2wMS9M6", + _type: "StatusChange.Opportunity", + opportunity_id: "oppo_8H4sjNso7FyBFaeR3RXi5P", + lead_id: "lead_9k2R7wYtVbXmS3Ac6Fq1Zp", + old_status: { id: "stat_3FD9DnGUCJzccBKTh8LiiK", label: "Active", type: "active" }, + new_status: { id: "stat_wMS9M6HC2O3CSEOzF5g2vE", label: "Won", type: "won" }, + organization_id: "orga_Hn4Wq7VtRj2AmY8bKzXc1S", + created_by: "user_Vp3Km9RtWzY6QaX1Nc4Bf7", + created_by_name: "Sam Okafor", + date_created: "2026-07-10T17:20:05.331000+00:00", + date_updated: "2026-07-10T17:20:05.331000+00:00", + }, + meta: { + request_method: "PUT", + request_path: "/api/v1/opportunity/oppo_8H4sjNso7FyBFaeR3RXi5P/", + }, + }, + subscription_id: "whsub_Km9RtWzY6QaX1Nc4Bf7Hn4", + }, + docsUrl: "https://developer.close.com/api/resources/activities/opportunity-status-changes.md", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "close-crm", + providerLabel: "Close", + eventType: "opportunity.updated", + name: "Opportunity updated (won)", + description: + "Close's own published webhook example: an opportunity's status field changed to \"Won\"; `previous_data` carries the prior status. Re-dated from the docs example, otherwise verbatim.", + body: { + event: { + id: "ev_2sYKRjcrA79yKxi3S4Crd7", + date_created: "2026-07-10T18:48:23.395000", + date_updated: "2026-07-10T18:48:23.395000", + object_type: "opportunity", + object_id: "oppo_7H4sjNso7FyBFaeR3RXi5PMJbilfo0c6UPCxsJtEhCO", + lead_id: "lead_zwqYhEFwzPyfCErS8uQ77is2wFLvr9BgVi6cTfbFM68", + organization_id: "orga_XbVPx5fFbKlYTz9PW5Ih1XDhViV10YihIaEgMEb6fVW", + user_id: "user_N6KhMpzHRCYQHdn4gRNIFNN5JExnsrprKA6ekxM63XA", + request_id: "req_4S2L8JTBAA1OUS74SVmfbN", + action: "updated", + changed_fields: ["confidence", "date_updated", "status_id", "status_label", "status_type"], + previous_data: { + status_type: "active", + confidence: 70, + date_updated: "2026-07-10T18:47:39.873000+00:00", + status_id: "stat_3FD9DnGUCJzccBKTh8LiiKoyVPpMJsOkJdcGoA5AYKH", + status_label: "Active", + }, + data: { + id: "oppo_8H4sjNso7FyBFaeR3RXi5PMJbilfo0c6UPCxsJtEhCO", + lead_id: "lead_zwqYhEFwzPyfCErS8uQ77is2wFLvr9BgVi6cTfbFM68", + lead_name: "KLine", + contact_id: "cont_BwlwYQkIP6AooiXP1CMvc6Zbb5gGh2gPu4dqIDlDrII", + contact_name: "Mr. Jones", + organization_id: "orga_XbVPx5fFbKlYTz9PW5Ih1XDhViV10YihIaEgMEb6fVW", + status_id: "stat_wMS9M6HC2O3CSEOzF5g2vEGt6RM5R3RfhIQixdnmjf2", + status_label: "Won", + status_type: "won", + confidence: 100, + value: 100000, + value_period: "one_time", + value_currency: "USD", + value_formatted: "$1,000", + note: "", + date_won: "2026-07-10", + date_lost: null, + created_by: "user_N6KhMpzHRCYQHdn4gRNIFNN5JExnsrprKA6ekxM63XA", + created_by_name: "Joe Kemp", + updated_by: "user_N6KhMpzHRCYQHdn4gRNIFNN5JExnsrprKA6ekxM63XA", + updated_by_name: "Joe Kemp", + user_id: "user_N6KhMpzHRCYQHdn4gRNIFNN5JExnsrprKA6ekxM63XA", + user_name: "Joe Kemp", + date_created: "2026-07-10T18:41:24.496000+00:00", + date_updated: "2026-07-10T18:48:23.392000+00:00", + }, + meta: { + request_method: "PUT", + request_path: "/api/v1/opportunity/oppo_7H4sjNso7FyBFaeR3RXi5PMJbilfo0c6UPCxsJtEhCO/", + }, + }, + subscription_id: "whsub_8AmjKCZYT3zI8eZoi4HhFC", + }, + docsUrl: "https://developer.close.com/topics/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/deepgram.ts b/internal-packages/webhook-sources/src/handAuthored/deepgram.ts new file mode 100644 index 00000000000..ab29ddc5800 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/deepgram.ts @@ -0,0 +1,304 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Deepgram prerecorded-transcription callback samples. When a `callback` URL is supplied to the + * `/v1/listen` endpoint, Deepgram POSTs the finished job's result to that URL - the same JSON shape + * the synchronous API would have returned, `{ metadata, results }`, with no `type`/`event` discriminant + * anywhere in the body. Auth is a `dg-token` header identifying (not authenticating) the API key used; + * there is no HMAC/Ed25519 signature, so these ship sample-only (no `presetId`). A failed job instead + * delivers Deepgram's general API error shape, `{ err_code, err_msg, request_id }`. `eventType` here is + * our own descriptive label (there is no wire value to mirror), distinguishing the shapes an integrator + * actually needs to branch on: plain mono, multichannel, diarized/utterance-level, and failure. + */ +export const samples: SampleRecord[] = [ + { + provider: "deepgram", + providerLabel: "Deepgram", + eventType: "transcript.completed", + name: "Transcript completed", + description: + "A prerecorded mono transcription job finished. `results.channels[].alternatives[0]` carries the transcript, overall confidence, and per-word timing.", + body: { + metadata: { + transaction_key: "deprecated", + request_id: "c22bea24-ecd3-4b53-bcbf-8ef087d905a5", + sha256: "d3b8f2a1c9e4567890abcdef1234567890abcdef1234567890abcdef123456", + created: "2026-07-14T09:32:11.418Z", + duration: 18.42, + channels: 1, + models: ["30089e05-99d1-4376-b32e-c263170674af"], + model_info: { + "30089e05-99d1-4376-b32e-c263170674af": { + name: "2-general-nova", + version: "2024-01-09.29447", + arch: "nova-2", + }, + }, + }, + results: { + channels: [ + { + alternatives: [ + { + transcript: "Thanks for calling support, how can I help you today?", + confidence: 0.9942, + words: [ + { + word: "thanks", + start: 0.08, + end: 0.32, + confidence: 0.9976, + punctuated_word: "Thanks", + }, + { + word: "for", + start: 0.32, + end: 0.48, + confidence: 0.9991, + punctuated_word: "for", + }, + { + word: "calling", + start: 0.48, + end: 0.8, + confidence: 0.9958, + punctuated_word: "calling", + }, + { + word: "support", + start: 0.8, + end: 1.24, + confidence: 0.9967, + punctuated_word: "support,", + }, + { + word: "how", + start: 1.4, + end: 1.56, + confidence: 0.9989, + punctuated_word: "how", + }, + { + word: "can", + start: 1.56, + end: 1.68, + confidence: 0.999, + punctuated_word: "can", + }, + { word: "i", start: 1.68, end: 1.76, confidence: 0.9984, punctuated_word: "I" }, + { + word: "help", + start: 1.76, + end: 2.0, + confidence: 0.9979, + punctuated_word: "help", + }, + { + word: "you", + start: 2.0, + end: 2.16, + confidence: 0.9985, + punctuated_word: "you", + }, + { + word: "today", + start: 2.16, + end: 2.48, + confidence: 0.9971, + punctuated_word: "today?", + }, + ], + }, + ], + }, + ], + }, + }, + docsUrl: "https://developers.deepgram.com/docs/callback", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "deepgram", + providerLabel: "Deepgram", + eventType: "transcript.completed-multichannel", + name: "Transcript completed (multichannel)", + description: + "A stereo recording transcribed with `multichannel=true`. `results.channels` has one entry per input channel (here, a two-line call recording with the agent on channel 0 and the customer on channel 1), each with its own alternatives.", + body: { + metadata: { + transaction_key: "deprecated", + request_id: "6f3a9e2d-4b1c-4a8e-9d5f-2c8b7a6e5d4c", + sha256: "9a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f", + created: "2026-07-13T15:04:52.907Z", + duration: 42.19, + channels: 2, + models: ["30089e05-99d1-4376-b32e-c263170674af"], + model_info: { + "30089e05-99d1-4376-b32e-c263170674af": { + name: "2-phonecall-nova", + version: "2024-01-09.29447", + arch: "nova-2", + }, + }, + }, + results: { + channels: [ + { + alternatives: [ + { + transcript: "Support line, this is Marcus, what's the account number on file?", + confidence: 0.9861, + words: [ + { + word: "support", + start: 0.0, + end: 0.36, + confidence: 0.9944, + punctuated_word: "Support", + }, + { + word: "line", + start: 0.36, + end: 0.6, + confidence: 0.9902, + punctuated_word: "line,", + }, + ], + }, + ], + }, + { + alternatives: [ + { + transcript: "Hi, it's ACC-58213, I can't access my dashboard.", + confidence: 0.9714, + words: [ + { word: "hi", start: 3.1, end: 3.24, confidence: 0.9931, punctuated_word: "Hi," }, + { + word: "it's", + start: 3.32, + end: 3.48, + confidence: 0.9822, + punctuated_word: "it's", + }, + ], + }, + ], + }, + ], + }, + }, + docsUrl: "https://developers.deepgram.com/docs/callback", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "deepgram", + providerLabel: "Deepgram", + eventType: "transcript.completed-diarized", + name: "Transcript completed (diarized)", + description: + "A single-channel recording transcribed with `diarize=true` and `utterances=true`. Each word carries a `speaker` index, and `results.utterances` groups words into per-speaker turns.", + body: { + metadata: { + transaction_key: "deprecated", + request_id: "b1c8e4a7-3d92-4f6e-8a1b-5e9c7d2f4a83", + sha256: "5e4d3c2b1a0f9e8d7c6b5a4938271605f4e3d2c1b0a9f8e7d6c5b4a39281706", + created: "2026-07-12T11:47:03.220Z", + duration: 61.05, + channels: 1, + models: ["e2f5b1c9-8a3d-4e7f-9c1a-2b6d8f4e5a70"], + model_info: { + "e2f5b1c9-8a3d-4e7f-9c1a-2b6d8f4e5a70": { + name: "3-general", + version: "2024-11-13.0", + arch: "nova-3", + }, + }, + }, + results: { + channels: [ + { + alternatives: [ + { + transcript: + "Let's kick off the standup. I finished the migration script yesterday. Nice, I'll review the PR this morning.", + confidence: 0.9788, + words: [ + { + word: "let's", + start: 0.04, + end: 0.28, + confidence: 0.9912, + punctuated_word: "Let's", + speaker: 0, + }, + { + word: "kick", + start: 0.28, + end: 0.48, + confidence: 0.9955, + punctuated_word: "kick", + speaker: 0, + }, + { + word: "i", + start: 3.12, + end: 3.2, + confidence: 0.988, + punctuated_word: "I", + speaker: 1, + }, + { + word: "finished", + start: 3.2, + end: 3.56, + confidence: 0.9903, + punctuated_word: "finished", + speaker: 1, + }, + ], + }, + ], + }, + ], + utterances: [ + { + start: 0.04, + end: 2.9, + confidence: 0.9912, + channel: 0, + transcript: "Let's kick off the standup.", + speaker: 0, + id: "3f2a1b0c-9d8e-4f7a-6b5c-4d3e2f1a0b9c", + }, + { + start: 3.12, + end: 5.64, + confidence: 0.9903, + channel: 0, + transcript: "I finished the migration script yesterday.", + speaker: 1, + id: "8c7b6a5d-4e3f-2a1b-0c9d-8e7f6a5b4c3d", + }, + ], + }, + }, + docsUrl: "https://developers.deepgram.com/docs/callback", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "deepgram", + providerLabel: "Deepgram", + eventType: "transcript.failed", + name: "Transcript failed", + description: + "Deepgram could not complete the job (here, fetching the source audio URL failed) and delivers its general API error shape to the callback URL instead of a results object.", + body: { + err_code: "REMOTE_CONTENT_ERROR", + err_msg: "There was an error fetching the audio from the provided URL.", + request_id: "a4e8d1c6-2f9b-4a3e-8d5c-1b6f9e2a7c4d", + }, + docsUrl: "https://developers.deepgram.com/docs/callback", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/discord.ts b/internal-packages/webhook-sources/src/handAuthored/discord.ts new file mode 100644 index 00000000000..31f9af8fd01 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/discord.ts @@ -0,0 +1,150 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Discord Webhook Events samples. Every delivery (including the initial `PING` handshake) shares + * the outer envelope `{ version, application_id, type, event? }`; `type: 0` is the PING handshake + * with no `event` field, `type: 1` carries `event: { type, timestamp, data }`. `QUEST_USER_ENROLLMENT` + * is documented but explicitly marked as not currently deliverable to apps, so it is excluded here. + */ +export const samples: SampleRecord[] = [ + { + provider: "discord", + providerLabel: "Discord", + presetId: "discord", + eventType: "PING", + name: "Webhook handshake PING", + description: + "Sent once when you save a webhook events URL in the developer portal, to confirm you can verify and respond in time.", + body: { + version: 1, + application_id: "1102345678901234567", + type: 0, + }, + docsUrl: "https://docs.discord.com/developers/events/webhook-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "discord", + providerLabel: "Discord", + presetId: "discord", + eventType: "APPLICATION_AUTHORIZED", + name: "App authorized to a server", + description: "A user installed and authorized the app to a guild (server).", + body: { + version: 1, + application_id: "1102345678901234567", + type: 1, + event: { + type: "APPLICATION_AUTHORIZED", + timestamp: "2026-07-14T18:22:03.064834", + data: { + integration_type: 0, + scopes: ["applications.commands", "bot"], + user: { + id: "912233445566778899", + username: "quinn.dev", + discriminator: "0", + avatar: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + global_name: "Quinn", + }, + guild: { + id: "445566778899001122", + name: "Trigger.dev Community", + icon: "f2e1d0c9b8a7968574635241302f1e0d", + }, + }, + }, + }, + docsUrl: "https://docs.discord.com/developers/events/webhook-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "discord", + providerLabel: "Discord", + presetId: "discord", + eventType: "APPLICATION_DEAUTHORIZED", + name: "App deauthorized by a user", + description: + "A user removed the app's authorization (uninstalled it from their account or a server).", + body: { + version: 1, + application_id: "1102345678901234567", + type: 1, + event: { + type: "APPLICATION_DEAUTHORIZED", + timestamp: "2026-07-14T19:05:41.221009", + data: { + user: { + id: "912233445566778899", + username: "quinn.dev", + discriminator: "0", + avatar: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + global_name: "Quinn", + }, + }, + }, + }, + docsUrl: "https://docs.discord.com/developers/events/webhook-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "discord", + providerLabel: "Discord", + presetId: "discord", + eventType: "ENTITLEMENT_CREATE", + name: "Premium entitlement created", + description: "A user purchased or was granted an application subscription entitlement.", + body: { + version: 1, + application_id: "1102345678901234567", + type: 1, + event: { + type: "ENTITLEMENT_CREATE", + timestamp: "2026-07-14T20:11:09.109604", + data: { + id: "1201983746501928374", + application_id: "1102345678901234567", + sku_id: "1091827364501928374", + user_id: "912233445566778899", + type: 8, + deleted: false, + consumed: false, + gift_code_flags: 0, + promotion_id: null, + }, + }, + }, + docsUrl: "https://docs.discord.com/developers/events/webhook-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "discord", + providerLabel: "Discord", + presetId: "discord", + eventType: "ENTITLEMENT_DELETE", + name: "Premium entitlement revoked", + description: "An entitlement was deleted, typically because of a refund or chargeback.", + body: { + version: 1, + application_id: "1102345678901234567", + type: 1, + event: { + type: "ENTITLEMENT_DELETE", + timestamp: "2026-07-14T21:47:33.552817", + data: { + id: "1201983746501928374", + application_id: "1102345678901234567", + sku_id: "1091827364501928374", + user_id: "912233445566778899", + type: 8, + deleted: true, + consumed: false, + gift_code_flags: 0, + promotion_id: null, + }, + }, + }, + docsUrl: "https://docs.discord.com/developers/events/webhook-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/docusign.ts b/internal-packages/webhook-sources/src/handAuthored/docusign.ts new file mode 100644 index 00000000000..401af2ec522 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/docusign.ts @@ -0,0 +1,278 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * DocuSign Connect samples, JSON SIM (Connect 2.0) envelope format. No preset: DocuSign signs with + * `X-DocuSign-Signature-1` (additional secrets add `-2`, `-3`, ...), an HMAC-SHA256 base64 digest of the + * raw JSON body keyed by the Connect configuration's HMAC secret. There is no `t=`/`v1=` component and + * no request-URL-inclusive signing, so this doesn't match our stripe, github, svix, square, or discord + * presets. The exact set of fields present under `data.envelopeSummary` depends on the subscriber's + * Connect configuration (which optional data fields were selected), so field presence can vary in + * practice; the shapes below reflect a typical default configuration. + */ +export const samples: SampleRecord[] = [ + { + provider: "docusign", + providerLabel: "DocuSign", + eventType: "envelope-sent", + name: "Envelope sent", + description: "An envelope was sent to its recipients for signing.", + body: { + event: "envelope-sent", + apiVersion: "v2.1", + uri: "/restapi/v2.1/accounts/8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c", + retryCount: 0, + configurationId: 271828, + generatedDateTime: "2026-06-28T15:02:11.4471203Z", + data: { + accountId: "8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d", + userId: "1e6b9c3f-8d2a-4c7e-9b1d-6a3f8c2e5d9b", + envelopeId: "3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c", + envelopeSummary: { + status: "sent", + emailSubject: "Please DocuSign: Master Services Agreement.pdf", + emailBlurb: "Please review and sign the attached agreement at your earliest convenience.", + envelopeIdStamping: "true", + signingLocation: "online", + allowMarkup: "false", + allowReassign: "true", + createdDateTime: "2026-06-28T15:01:42.0000000Z", + sentDateTime: "2026-06-28T15:02:11.0000000Z", + statusChangedDateTime: "2026-06-28T15:02:11.0000000Z", + documentsUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/documents", + recipientsUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/recipients", + attachmentsUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/attachments", + envelopeUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c", + customFieldsUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/custom_fields", + notificationUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/notification", + recipients: { + signers: [ + { + recipientId: "1", + recipientIdGuid: "5c2a8d1e-7f3b-4a6c-9e2d-8b1f5a3c7e9d", + name: "Jordan Reyes", + email: "jordan.reyes@example.com", + recipientType: "signer", + routingOrder: "1", + status: "sent", + deliveryMethod: "email", + sentDateTime: "2026-06-28T15:02:11.0000000Z", + }, + ], + }, + }, + }, + }, + docsUrl: "https://developers.docusign.com/platform/webhooks/connect/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "docusign", + providerLabel: "DocuSign", + eventType: "envelope-completed", + name: "Envelope completed", + description: + "All recipients on an envelope have finished signing and the envelope is complete.", + body: { + event: "envelope-completed", + apiVersion: "v2.1", + uri: "/restapi/v2.1/accounts/8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c", + retryCount: 0, + configurationId: 271828, + generatedDateTime: "2026-06-28T15:11:47.9012345Z", + data: { + accountId: "8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d", + userId: "1e6b9c3f-8d2a-4c7e-9b1d-6a3f8c2e5d9b", + envelopeId: "3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c", + envelopeSummary: { + status: "completed", + emailSubject: "Please DocuSign: Master Services Agreement.pdf", + createdDateTime: "2026-06-28T15:01:42.0000000Z", + sentDateTime: "2026-06-28T15:02:11.0000000Z", + deliveredDateTime: "2026-06-28T15:05:03.0000000Z", + completedDateTime: "2026-06-28T15:11:47.0000000Z", + statusChangedDateTime: "2026-06-28T15:11:47.0000000Z", + documentsUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/documents", + recipientsUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/recipients", + certificateUri: "/envelopes/3f9a2c1e-6b4d-4e2f-8a1c-9d3b5f7e2a6c/documents/certificate", + recipients: { + signers: [ + { + recipientId: "1", + recipientIdGuid: "5c2a8d1e-7f3b-4a6c-9e2d-8b1f5a3c7e9d", + name: "Jordan Reyes", + email: "jordan.reyes@example.com", + recipientType: "signer", + routingOrder: "1", + status: "completed", + deliveryMethod: "email", + sentDateTime: "2026-06-28T15:02:11.0000000Z", + deliveredDateTime: "2026-06-28T15:05:03.0000000Z", + signedDateTime: "2026-06-28T15:11:47.0000000Z", + }, + ], + }, + }, + }, + }, + docsUrl: "https://developers.docusign.com/platform/webhooks/connect/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "docusign", + providerLabel: "DocuSign", + eventType: "envelope-declined", + name: "Envelope declined", + description: "A recipient declined to sign, which moves the whole envelope to declined.", + body: { + event: "envelope-declined", + apiVersion: "v2.1", + uri: "/restapi/v2.1/accounts/8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d/envelopes/7a1d4c9e-2b6f-4d8a-9e3c-5f1b8a2d6c9e", + retryCount: 0, + configurationId: 271828, + generatedDateTime: "2026-07-02T09:47:03.1120987Z", + data: { + accountId: "8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d", + userId: "1e6b9c3f-8d2a-4c7e-9b1d-6a3f8c2e5d9b", + envelopeId: "7a1d4c9e-2b6f-4d8a-9e3c-5f1b8a2d6c9e", + envelopeSummary: { + status: "declined", + emailSubject: "Please DocuSign: Contractor Agreement.pdf", + createdDateTime: "2026-07-02T09:30:00.0000000Z", + sentDateTime: "2026-07-02T09:31:04.0000000Z", + declinedDateTime: "2026-07-02T09:47:03.0000000Z", + statusChangedDateTime: "2026-07-02T09:47:03.0000000Z", + documentsUri: "/envelopes/7a1d4c9e-2b6f-4d8a-9e3c-5f1b8a2d6c9e/documents", + recipientsUri: "/envelopes/7a1d4c9e-2b6f-4d8a-9e3c-5f1b8a2d6c9e/recipients", + recipients: { + signers: [ + { + recipientId: "1", + recipientIdGuid: "9d3b5f7e-2a6c-4e8d-9b1f-5a3c7e9d2b6a", + name: "Morgan Alvarez", + email: "morgan.alvarez@example.com", + recipientType: "signer", + routingOrder: "1", + status: "declined", + deliveryMethod: "email", + sentDateTime: "2026-07-02T09:31:04.0000000Z", + declinedDateTime: "2026-07-02T09:47:03.0000000Z", + declineReason: "Terms need revision before I can sign.", + }, + ], + }, + }, + }, + }, + docsUrl: "https://developers.docusign.com/platform/webhooks/connect/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "docusign", + providerLabel: "DocuSign", + eventType: "envelope-voided", + name: "Envelope voided", + description: + "The sender voided the envelope before it was completed, invalidating it for all recipients.", + body: { + event: "envelope-voided", + apiVersion: "v2.1", + uri: "/restapi/v2.1/accounts/8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d/envelopes/2e5a8c1d-6f3b-4a9e-8d2c-7b1f5a3e9c6d", + retryCount: 0, + configurationId: 271828, + generatedDateTime: "2026-07-05T18:22:39.5560321Z", + data: { + accountId: "8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d", + userId: "1e6b9c3f-8d2a-4c7e-9b1d-6a3f8c2e5d9b", + envelopeId: "2e5a8c1d-6f3b-4a9e-8d2c-7b1f5a3e9c6d", + envelopeSummary: { + status: "voided", + emailSubject: "Please DocuSign: Vendor Renewal.pdf", + createdDateTime: "2026-07-04T10:15:00.0000000Z", + sentDateTime: "2026-07-04T10:16:22.0000000Z", + voidedDateTime: "2026-07-05T18:22:39.0000000Z", + statusChangedDateTime: "2026-07-05T18:22:39.0000000Z", + voidedReason: "Superseded by a corrected version of this document.", + documentsUri: "/envelopes/2e5a8c1d-6f3b-4a9e-8d2c-7b1f5a3e9c6d/documents", + recipientsUri: "/envelopes/2e5a8c1d-6f3b-4a9e-8d2c-7b1f5a3e9c6d/recipients", + recipients: { + signers: [ + { + recipientId: "1", + recipientIdGuid: "3c7e9d2b-6a5f-4c8e-9d1b-2a6f5c9e3d7b", + name: "Priya Nair", + email: "priya.nair@example.com", + recipientType: "signer", + routingOrder: "1", + status: "sent", + deliveryMethod: "email", + sentDateTime: "2026-07-04T10:16:22.0000000Z", + }, + ], + }, + }, + }, + }, + docsUrl: "https://developers.docusign.com/platform/webhooks/connect/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "docusign", + providerLabel: "DocuSign", + eventType: "recipient-completed", + name: "Recipient completed", + description: + "One recipient finished their signing action; other recipients on the envelope may still be pending.", + body: { + event: "recipient-completed", + apiVersion: "v2.1", + uri: "/restapi/v2.1/accounts/8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d/envelopes/9b2d5f8a-1c6e-4d3b-8a9c-2e5f7b1d9a3c", + retryCount: 0, + configurationId: 271828, + generatedDateTime: "2026-07-08T13:05:52.7789456Z", + data: { + accountId: "8675309a-1234-4a1b-9c1d-3e5f7a9b1c2d", + userId: "1e6b9c3f-8d2a-4c7e-9b1d-6a3f8c2e5d9b", + envelopeId: "9b2d5f8a-1c6e-4d3b-8a9c-2e5f7b1d9a3c", + recipientId: "1", + envelopeSummary: { + status: "sent", + emailSubject: "Please DocuSign: Offer Letter.pdf", + createdDateTime: "2026-07-08T12:50:10.0000000Z", + sentDateTime: "2026-07-08T12:51:03.0000000Z", + statusChangedDateTime: "2026-07-08T13:05:52.0000000Z", + documentsUri: "/envelopes/9b2d5f8a-1c6e-4d3b-8a9c-2e5f7b1d9a3c/documents", + recipientsUri: "/envelopes/9b2d5f8a-1c6e-4d3b-8a9c-2e5f7b1d9a3c/recipients", + recipients: { + signers: [ + { + recipientId: "1", + recipientIdGuid: "7b1f5a3e-9c6d-4b2a-8e5f-3c9d7b1a5e2f", + name: "Sam Okafor", + email: "sam.okafor@example.com", + recipientType: "signer", + routingOrder: "1", + status: "completed", + deliveryMethod: "email", + sentDateTime: "2026-07-08T12:51:03.0000000Z", + deliveredDateTime: "2026-07-08T12:58:41.0000000Z", + signedDateTime: "2026-07-08T13:05:52.0000000Z", + }, + { + recipientId: "2", + recipientIdGuid: "5e2f7b1a-9d3c-4e8b-9a2d-6c5f1a7e3b9d", + name: "Dana Whitfield", + email: "dana.whitfield@example.com", + recipientType: "signer", + routingOrder: "2", + status: "created", + deliveryMethod: "email", + }, + ], + }, + }, + }, + }, + docsUrl: "https://developers.docusign.com/platform/webhooks/connect/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/elevenlabs.ts b/internal-packages/webhook-sources/src/handAuthored/elevenlabs.ts new file mode 100644 index 00000000000..26e6b6e43b3 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/elevenlabs.ts @@ -0,0 +1,271 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * ElevenLabs Conversational AI post-call samples. No preset: ElevenLabs signs with + * `elevenlabs-signature: t=,v0=` (HMAC-SHA256 over `{timestamp}.{rawBody}`), which is close + * to but does not exactly match our `stripe` preset's header name or `v1=` value prefix. Every body is + * the thin `{ type, event_timestamp, data }` envelope; `post_call_audio` carries only three fields by + * design (no transcript/metadata/analysis). + */ +export const samples: SampleRecord[] = [ + { + provider: "elevenlabs", + providerLabel: "ElevenLabs", + eventType: "post_call_transcription", + name: "Post-call transcription (successful call)", + description: + "Full conversation data delivered after a call ends: transcript turns, call metadata, and analysis results.", + body: { + type: "post_call_transcription", + event_timestamp: 1752562811, + data: { + agent_id: "xI1AVR5jrfLZ6mSjxgTS", + agent_name: "Support Agent", + conversation_id: "conv_4f9b2e7c1a8d6f3e0b5c", + status: "done", + user_id: "user_8a2d5f1c9e7b3a6d", + authorization_method: "signed_url", + transcript: [ + { + role: "agent", + message: "Hi, thanks for calling. How can I help you today?", + tool_calls: null, + tool_results: null, + feedback: null, + time_in_call_secs: 0, + conversation_turn_metrics: null, + }, + { + role: "user", + message: "I need to check the status of my last order.", + tool_calls: null, + tool_results: null, + feedback: null, + time_in_call_secs: 4, + conversation_turn_metrics: null, + }, + { + role: "agent", + message: "Your order shipped yesterday and should arrive by Friday.", + tool_calls: null, + tool_results: null, + feedback: null, + time_in_call_secs: 9, + conversation_turn_metrics: null, + }, + ], + metadata: { + start_time_unix_secs: 1752562780, + call_duration_secs: 31, + cost: 412, + deletion_settings: { + deletion_time_unix_secs: null, + deleted_logs_at_time_unix_secs: null, + deleted_audio_at_time_unix_secs: null, + deleted_transcript_at_time_unix_secs: null, + delete_transcript_and_pii: false, + delete_audio: false, + }, + feedback: { + overall_score: null, + likes: 0, + dislikes: 0, + }, + authorization_method: "signed_url", + charging: { + dev_discount: false, + }, + termination_reason: "the conversation was ended by the user", + }, + analysis: { + evaluation_criteria_results: {}, + data_collection_results: {}, + call_successful: "success", + transcript_summary: + "Caller asked for their order status; agent confirmed shipping and ETA.", + }, + conversation_initiation_client_data: { + dynamic_variables: { + customer_name: "Priya Nair", + }, + }, + }, + }, + docsUrl: "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "elevenlabs", + providerLabel: "ElevenLabs", + eventType: "post_call_transcription", + name: "Post-call transcription (tool call, evaluation failed)", + description: + "A transcript where the agent invoked a tool mid-call and the post-call evaluation criteria did not pass.", + body: { + type: "post_call_transcription", + event_timestamp: 1752563912, + data: { + agent_id: "xI1AVR5jrfLZ6mSjxgTS", + agent_name: "Booking Agent", + conversation_id: "conv_9c3a7e1f5b2d8c4a6f0e", + status: "done", + user_id: "user_1b6e9a3f7c2d5b8e", + authorization_method: "public", + transcript: [ + { + role: "agent", + message: "Let me look up available slots for you.", + tool_calls: [ + { + tool_name: "check_availability", + tool_call_id: "tool_call_7d2e4c1b", + params_as_json: '{"date":"2026-07-20"}', + }, + ], + tool_results: null, + feedback: null, + time_in_call_secs: 6, + conversation_turn_metrics: null, + }, + { + role: "agent", + message: "There are no slots left on that date, unfortunately.", + tool_calls: null, + tool_results: [ + { + tool_name: "check_availability", + tool_call_id: "tool_call_7d2e4c1b", + result_value: '{"slots":[]}', + }, + ], + feedback: null, + time_in_call_secs: 8, + conversation_turn_metrics: null, + }, + ], + metadata: { + start_time_unix_secs: 1752563890, + call_duration_secs: 22, + cost: 298, + deletion_settings: { + deletion_time_unix_secs: null, + deleted_logs_at_time_unix_secs: null, + deleted_audio_at_time_unix_secs: null, + deleted_transcript_at_time_unix_secs: null, + delete_transcript_and_pii: false, + delete_audio: false, + }, + feedback: { + overall_score: 2, + likes: 0, + dislikes: 1, + }, + authorization_method: "public", + charging: { + dev_discount: false, + }, + termination_reason: "client disconnected", + }, + analysis: { + evaluation_criteria_results: { + resolved_user_request: { + result: "failure", + rationale: "No booking slot was found or offered as an alternative.", + }, + }, + data_collection_results: {}, + call_successful: "failure", + transcript_summary: + "Caller wanted to book a slot; none were available and no alternative was offered.", + }, + conversation_initiation_client_data: { + dynamic_variables: { + requested_date: "2026-07-20", + }, + }, + }, + }, + docsUrl: "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "elevenlabs", + providerLabel: "ElevenLabs", + eventType: "post_call_audio", + name: "Post-call audio", + description: + "Minimal payload delivered separately from transcription webhooks, carrying only the base64-encoded MP3 of the full call. No transcript, metadata, or analysis fields are included.", + body: { + type: "post_call_audio", + event_timestamp: 1752562819, + data: { + agent_id: "xI1AVR5jrfLZ6mSjxgTS", + conversation_id: "conv_4f9b2e7c1a8d6f3e0b5c", + full_audio: + "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjI5LjEwMAAAAAAAAAAAAAAA//sample-not-real-audio-bytes", + }, + }, + docsUrl: "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "elevenlabs", + providerLabel: "ElevenLabs", + eventType: "call_initiation_failure", + name: "Call initiation failure (SIP, busy)", + description: + "The outbound call never connected because the SIP trunk reported the line as busy. `data.metadata.body` carries the SIP-specific failure detail.", + body: { + type: "call_initiation_failure", + event_timestamp: 1752564001, + data: { + agent_id: "xI1AVR5jrfLZ6mSjxgTS", + conversation_id: "conv_2e8c5a1f9b3d7c4e6a0f", + failure_reason: "busy", + metadata: { + type: "sip", + body: { + from_number: 15550001234, + to_number: 15550005678, + sip_status_code: 486, + error_reason: "Busy Here", + call_sid: "sip_call_3a7e1c9d5b2f", + sip_status: "486 Busy Here", + twirp_code: "unavailable", + }, + }, + }, + }, + docsUrl: "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "elevenlabs", + providerLabel: "ElevenLabs", + eventType: "call_initiation_failure", + name: "Call initiation failure (Twilio, no answer)", + description: + "The outbound call was routed through Twilio and rang without being answered. `data.metadata.body` follows Twilio's StatusCallback field names.", + body: { + type: "call_initiation_failure", + event_timestamp: 1752564205, + data: { + agent_id: "xI1AVR5jrfLZ6mSjxgTS", + conversation_id: "conv_7b1d4f9a2c6e8b3d5f0a", + failure_reason: "no-answer", + metadata: { + type: "twilio", + body: { + Called: "+15550005678", + Caller: "+15550001234", + CallSid: "CAfake1234567890abcdef1234567890", + CallStatus: "no-answer", + SipResponseCode: "480", + }, + }, + }, + }, + docsUrl: "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/gitlab.ts b/internal-packages/webhook-sources/src/handAuthored/gitlab.ts new file mode 100644 index 00000000000..1d703e57bcf --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/gitlab.ts @@ -0,0 +1,270 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * GitLab's default webhook auth is a plaintext `X-Gitlab-Token` shared secret (no signature at all), + * which our hookdeck-sourced samples carry via extraHeaders and which does not map to any preset. + * GitLab also offers an opt-in "signing token" mode that follows the Standard Webhooks spec exactly: + * `webhook-id` / `webhook-timestamp` / `webhook-signature` headers, HMAC-SHA256 base64 over + * `{id}.{timestamp}.{body}`, secret prefixed `whsec_`. That mode matches our `svix` preset, so these + * samples are tagged `presetId: "svix"` and cover event types not already present in the hookdeck set + * (Merge Request Hook, Pipeline Hook, Job Hook). + */ +export const samples: SampleRecord[] = [ + { + provider: "gitlab", + providerLabel: "GitLab", + presetId: "svix", + eventType: "Merge Request Hook", + name: "Merge request opened", + description: "A merge request is opened against the target branch.", + body: { + object_kind: "merge_request", + event_type: "merge_request", + user: { + id: 1, + name: "Alex Garcia", + username: "agarcia", + avatar_url: "https://www.gravatar.com/avatar/1a29da0ccd0994?s=80&d=identicon", + email: "agarcia@example.com", + }, + project: { + id: 2, + name: "Flight Management", + description: "Flight management application for tracking aircraft status.", + web_url: "https://gitlab.example.com/flightjs/flight-management", + avatar_url: null, + git_ssh_url: "ssh://git@gitlab.example.com/flightjs/flight-management.git", + git_http_url: "https://gitlab.example.com/flightjs/flight-management.git", + namespace: "Flightjs", + visibility_level: 0, + path_with_namespace: "flightjs/flight-management", + default_branch: "main", + ci_config_path: null, + }, + object_attributes: { + id: 93, + iid: 16, + author_id: 1, + created_at: "2026-06-30 05:56:22 UTC", + updated_at: "2026-06-30 05:56:25 UTC", + title: "Add input validation to booking form", + description: "This merge request adds input validation to the booking form.", + draft: false, + state: "opened", + merge_status: "checking", + detailed_merge_status: "checking", + source_branch: "feature/booking-validation", + source_project_id: 2, + target_branch: "main", + target_project_id: 2, + target_branch_protected: true, + merge_commit_sha: null, + merged_at: null, + merge_error: null, + merge_when_pipeline_succeeds: false, + milestone_id: 8, + assignee_ids: [1], + reviewer_ids: [25], + labels: [{ id: 19, title: "enhancement", color: "#adb21a" }], + last_commit: { + id: "e59094b8de0f2f91abbe4760a52d9137260252d8", + message: "Add email format validation", + timestamp: "2026-06-30T05:01:10+00:00", + url: "https://gitlab.example.com/flightjs/flight-management/-/commit/e59094b8de0f2f91abbe4760a52d9137260252d8", + author: { name: "Alex Garcia", email: "agarcia@example.com" }, + }, + url: "https://gitlab.example.com/flightjs/flight-management/-/merge_requests/16", + action: "open", + actioned_at: "2026-06-30 05:56:26 UTC", + }, + labels: [{ id: 19, title: "enhancement", color: "#adb21a" }], + changes: { + merge_status: { previous: "preparing", current: "checking" }, + updated_at: { previous: "2026-06-30 05:56:22 UTC", current: "2026-06-30 05:56:25 UTC" }, + }, + assignees: [ + { id: 1, name: "Alex Garcia", username: "agarcia", email: "agarcia@example.com" }, + ], + reviewers: [ + { + id: 25, + name: "Sidney Jones", + username: "sjones", + state: "unreviewed", + re_requested: false, + }, + ], + }, + extraHeaders: { + "x-gitlab-event": "Merge Request Hook", + "x-gitlab-instance": "https://gitlab.example.com", + }, + docsUrl: + "https://docs.gitlab.com/user/project/integrations/webhook_events/#merge-request-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "gitlab", + providerLabel: "GitLab", + presetId: "svix", + eventType: "Pipeline Hook", + name: "Pipeline succeeded", + description: "A CI/CD pipeline finishes with an overall success status.", + body: { + object_kind: "pipeline", + object_attributes: { + id: 31, + iid: 3, + name: "Pipeline for branch: main", + ref: "main", + tag: false, + sha: "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + before_sha: "959e04d7c7a30600b7a35cb4a53db4b0ae0f0433", + source: "push", + status: "success", + detailed_status: "passed", + stages: ["build", "test", "deploy"], + created_at: "2026-06-30 15:23:28 UTC", + finished_at: "2026-06-30 15:26:29 UTC", + duration: 63, + queued_duration: 10, + url: "https://gitlab.example.com/flightjs/flight-management/-/pipelines/31", + }, + merge_request: null, + user: { + id: 1, + name: "Alex Garcia", + username: "agarcia", + avatar_url: "https://www.gravatar.com/avatar/1a29da0ccd0994?s=80&d=identicon", + email: "agarcia@example.com", + }, + project: { + id: 2, + name: "Flight Management", + web_url: "https://gitlab.example.com/flightjs/flight-management", + namespace: "Flightjs", + visibility_level: 0, + path_with_namespace: "flightjs/flight-management", + default_branch: "main", + }, + commit: { + id: "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + message: "Add email format validation\n", + title: "Add email format validation", + timestamp: "2026-06-30T15:23:21+00:00", + url: "https://gitlab.example.com/flightjs/flight-management/-/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", + author: { name: "Alex Garcia", email: "agarcia@example.com" }, + }, + builds: [ + { + id: 380, + stage: "deploy", + name: "production", + status: "success", + created_at: "2026-06-30 15:23:28 UTC", + started_at: "2026-06-30 15:25:40 UTC", + finished_at: "2026-06-30 15:26:29 UTC", + duration: 49.0, + when: "manual", + manual: true, + allow_failure: false, + }, + { + id: 377, + stage: "test", + name: "test-image", + status: "success", + created_at: "2026-06-30 15:23:28 UTC", + started_at: "2026-06-30 15:24:12 UTC", + finished_at: "2026-06-30 15:24:29 UTC", + duration: 17.0, + when: "on_success", + manual: false, + allow_failure: false, + }, + ], + }, + extraHeaders: { + "x-gitlab-event": "Pipeline Hook", + "x-gitlab-instance": "https://gitlab.example.com", + }, + docsUrl: "https://docs.gitlab.com/user/project/integrations/webhook_events/#pipeline-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "gitlab", + providerLabel: "GitLab", + presetId: "svix", + eventType: "Job Hook", + name: "Job created", + description: "An individual CI/CD job within a pipeline changes state.", + body: { + object_kind: "build", + ref: "main", + tag: false, + before_sha: "2293ada6b400935a1378653304eaf6221e0fdb8f", + sha: "2293ada6b400935a1378653304eaf6221e0fdb8f", + retries_count: 0, + build_id: 1977, + build_name: "test-image", + build_stage: "test", + build_status: "created", + build_created_at: "2026-06-30T02:41:37.886Z", + build_started_at: null, + build_finished_at: null, + build_duration: null, + build_allow_failure: false, + build_failure_reason: "unknown_failure", + pipeline_id: 2366, + runner: { + id: 380987, + description: "shared-runner-6.gitlab.example.com", + runner_type: "instance_type", + active: true, + is_shared: true, + tags: ["linux", "docker"], + }, + project_id: 2, + project_name: "flightjs/flight-management", + user: { + id: 1, + name: "Alex Garcia", + username: "agarcia", + email: "agarcia@example.com", + }, + commit: { + id: 2366, + name: "Build pipeline", + sha: "2293ada6b400935a1378653304eaf6221e0fdb8f", + message: "Add email format validation\n", + author_name: "Alex Garcia", + author_email: "agarcia@example.com", + status: "created", + duration: null, + }, + repository: { + name: "flight-management", + url: "https://gitlab.example.com/flightjs/flight-management", + git_ssh_url: "ssh://git@gitlab.example.com/flightjs/flight-management.git", + git_http_url: "https://gitlab.example.com/flightjs/flight-management.git", + visibility_level: 0, + }, + project: { + id: 2, + name: "Flight Management", + web_url: "https://gitlab.example.com/flightjs/flight-management", + namespace: "Flightjs", + visibility_level: 0, + path_with_namespace: "flightjs/flight-management", + default_branch: "main", + }, + environment: null, + }, + extraHeaders: { + "x-gitlab-event": "Job Hook", + "x-gitlab-instance": "https://gitlab.example.com", + }, + docsUrl: "https://docs.gitlab.com/user/project/integrations/webhook_events/#job-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/index.ts b/internal-packages/webhook-sources/src/handAuthored/index.ts new file mode 100644 index 00000000000..5658e15017b --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/index.ts @@ -0,0 +1,85 @@ +/** GENERATED by catalog/build-aggregates.ts. Do not edit by hand. */ +import { type SampleRecord } from "../sampleRecord.js"; +import { samples as s_anthropic } from "./anthropic.js"; +import { samples as s_assemblyai } from "./assemblyai.js"; +import { samples as s_attio } from "./attio.js"; +import { samples as s_auth0 } from "./auth0.js"; +import { samples as s_brex } from "./brex.js"; +import { samples as s_cal_com } from "./cal-com.js"; +import { samples as s_calendly } from "./calendly.js"; +import { samples as s_clerk } from "./clerk.js"; +import { samples as s_close_crm } from "./close-crm.js"; +import { samples as s_deepgram } from "./deepgram.js"; +import { samples as s_discord } from "./discord.js"; +import { samples as s_docusign } from "./docusign.js"; +import { samples as s_elevenlabs } from "./elevenlabs.js"; +import { samples as s_gitlab } from "./gitlab.js"; +import { samples as s_intercom } from "./intercom.js"; +import { samples as s_jira } from "./jira.js"; +import { samples as s_linear } from "./linear.js"; +import { samples as s_notion } from "./notion.js"; +import { samples as s_openai } from "./openai.js"; +import { samples as s_pagerduty } from "./pagerduty.js"; +import { samples as s_plaid } from "./plaid.js"; +import { samples as s_postmark } from "./postmark.js"; +import { samples as s_recall_ai } from "./recall-ai.js"; +import { samples as s_replicate } from "./replicate.js"; +import { samples as s_resend } from "./resend.js"; +import { samples as s_retell } from "./retell.js"; +import { samples as s_sendgrid } from "./sendgrid.js"; +import { samples as s_sentry } from "./sentry.js"; +import { samples as s_slack } from "./slack.js"; +import { samples as s_square } from "./square.js"; +import { samples as s_supabase } from "./supabase.js"; +import { samples as s_telegram } from "./telegram.js"; +import { samples as s_twilio } from "./twilio.js"; +import { samples as s_typeform } from "./typeform.js"; +import { samples as s_vapi } from "./vapi.js"; +import { samples as s_vercel } from "./vercel.js"; +import { samples as s_whatsapp } from "./whatsapp.js"; +import { samples as s_workos } from "./workos.js"; +import { samples as s_zendesk } from "./zendesk.js"; +import { samples as s_zoom } from "./zoom.js"; + +export const handAuthoredSamples: SampleRecord[] = [ + ...s_anthropic, + ...s_assemblyai, + ...s_attio, + ...s_auth0, + ...s_brex, + ...s_cal_com, + ...s_calendly, + ...s_clerk, + ...s_close_crm, + ...s_deepgram, + ...s_discord, + ...s_docusign, + ...s_elevenlabs, + ...s_gitlab, + ...s_intercom, + ...s_jira, + ...s_linear, + ...s_notion, + ...s_openai, + ...s_pagerduty, + ...s_plaid, + ...s_postmark, + ...s_recall_ai, + ...s_replicate, + ...s_resend, + ...s_retell, + ...s_sendgrid, + ...s_sentry, + ...s_slack, + ...s_square, + ...s_supabase, + ...s_telegram, + ...s_twilio, + ...s_typeform, + ...s_vapi, + ...s_vercel, + ...s_whatsapp, + ...s_workos, + ...s_zendesk, + ...s_zoom, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/intercom.ts b/internal-packages/webhook-sources/src/handAuthored/intercom.ts new file mode 100644 index 00000000000..c4d3923485c --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/intercom.ts @@ -0,0 +1,388 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Intercom webhook notification samples. Every delivery shares the same envelope + * (`type: "notification_event"`, `topic`, `app_id`, `delivery_attempts`, `first_sent_at`, + * `data.item`); `eventType` mirrors `topic`, the real discriminant. No preset applies (Intercom + * signs with HMAC-SHA1 over `X-Hub-Signature`, not one of our supported schemes), so no + * `presetId` is set. + */ +export const samples: SampleRecord[] = [ + { + provider: "intercom", + providerLabel: "Intercom", + eventType: "conversation.user.created", + name: "Conversation created by user", + description: "A contact started a new conversation from the Messenger.", + body: { + type: "notification_event", + topic: "conversation.user.created", + id: "notif_5f27a9e0-0e33-4a1e-8f7c-2b6d4a9c11f0", + app_id: "a86dr8yl", + created_at: 1781524800, + delivery_attempts: 1, + first_sent_at: 1781524800, + data: { + item: { + type: "conversation", + id: "195208945002873", + created_at: 1781524800, + updated_at: 1781524800, + waiting_since: 1781524800, + snoozed_until: null, + source: { + type: "conversation", + id: "403982210", + delivered_as: "customer_initiated", + subject: "", + body: "

Hi, I'm having trouble connecting my Slack workspace to notifications.

", + author: { + type: "user", + id: "6620fa1e8f2b3c4d5e6f7890", + name: "Priya Desai", + email: "priya@northwind.example", + }, + attachments: [], + url: null, + redacted: false, + }, + contacts: { + type: "contact.list", + contacts: [{ type: "contact", id: "6620fa1e8f2b3c4d5e6f7890" }], + }, + teammates: null, + admin_assignee_id: null, + team_assignee_id: "5017691", + open: true, + state: "open", + read: false, + tags: { type: "tag.list", tags: [] }, + priority: "not_priority", + conversation_rating: null, + statistics: { + time_to_assignment: null, + time_to_admin_reply: null, + time_to_first_close: null, + time_to_last_close: null, + median_time_to_reply: null, + first_contact_reply_at: 1781524800, + first_assignment_at: null, + first_admin_reply_at: null, + first_close_at: null, + last_assignment_at: null, + last_contact_reply_at: 1781524800, + last_admin_reply_at: null, + last_close_at: null, + count_reopens: 0, + count_assignments: 0, + count_conversation_parts: 0, + }, + conversation_parts: { + type: "conversation_part.list", + conversation_parts: [], + total_count: 0, + }, + }, + }, + }, + docsUrl: "https://developers.intercom.com/docs/references/webhooks/webhook-models", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "intercom", + providerLabel: "Intercom", + eventType: "conversation.user.replied", + name: "User replied to a conversation", + description: "The contact who opened the conversation sent a follow-up reply.", + body: { + type: "notification_event", + topic: "conversation.user.replied", + id: "notif_8a41c6d2-4b8e-4f2a-9c3d-7e0f5a8b2d61", + app_id: "a86dr8yl", + created_at: 1781528440, + delivery_attempts: 1, + first_sent_at: 1781528440, + data: { + item: { + type: "conversation", + id: "195208945002873", + created_at: 1781524800, + updated_at: 1781528440, + waiting_since: 1781528440, + snoozed_until: null, + source: { + type: "conversation", + id: "403982210", + delivered_as: "customer_initiated", + subject: "", + body: "

Hi, I'm having trouble connecting my Slack workspace to notifications.

", + author: { + type: "user", + id: "6620fa1e8f2b3c4d5e6f7890", + name: "Priya Desai", + email: "priya@northwind.example", + }, + attachments: [], + url: null, + redacted: false, + }, + contacts: { + type: "contact.list", + contacts: [{ type: "contact", id: "6620fa1e8f2b3c4d5e6f7890" }], + }, + teammates: null, + admin_assignee_id: null, + team_assignee_id: "5017691", + open: true, + state: "open", + read: false, + tags: { type: "tag.list", tags: [] }, + priority: "not_priority", + conversation_rating: null, + statistics: { + time_to_assignment: null, + time_to_admin_reply: null, + time_to_first_close: null, + time_to_last_close: null, + median_time_to_reply: null, + first_contact_reply_at: 1781524800, + first_assignment_at: null, + first_admin_reply_at: null, + first_close_at: null, + last_assignment_at: null, + last_contact_reply_at: 1781528440, + last_admin_reply_at: null, + last_close_at: null, + count_reopens: 0, + count_assignments: 0, + count_conversation_parts: 1, + }, + conversation_parts: { + type: "conversation_part.list", + conversation_parts: [ + { + type: "conversation_part", + id: "403982298", + part_type: "comment", + body: "

Just to add, it fails right after I click Authorize.

", + created_at: 1781528440, + updated_at: 1781528440, + notified_at: 1781528440, + assigned_to: null, + author: { + type: "user", + id: "6620fa1e8f2b3c4d5e6f7890", + name: "Priya Desai", + email: "priya@northwind.example", + }, + attachments: [], + external_id: null, + redacted: false, + }, + ], + total_count: 1, + }, + }, + }, + }, + docsUrl: "https://developers.intercom.com/docs/references/webhooks/webhook-models", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "intercom", + providerLabel: "Intercom", + eventType: "conversation.admin.replied", + name: "Admin replied to a conversation", + description: "A teammate answered from the Intercom inbox.", + body: { + type: "notification_event", + topic: "conversation.admin.replied", + id: "notif_1c9e7b53-2d6a-49f0-8e5c-3a7d9f1b6c04", + app_id: "a86dr8yl", + created_at: 1781529100, + delivery_attempts: 1, + first_sent_at: 1781529100, + data: { + item: { + type: "conversation", + id: "195208945002873", + created_at: 1781524800, + updated_at: 1781529100, + waiting_since: null, + snoozed_until: null, + source: { + type: "conversation", + id: "403982210", + delivered_as: "customer_initiated", + subject: "", + body: "

Hi, I'm having trouble connecting my Slack workspace to notifications.

", + author: { + type: "user", + id: "6620fa1e8f2b3c4d5e6f7890", + name: "Priya Desai", + email: "priya@northwind.example", + }, + attachments: [], + url: null, + redacted: false, + }, + contacts: { + type: "contact.list", + contacts: [{ type: "contact", id: "6620fa1e8f2b3c4d5e6f7890" }], + }, + teammates: { type: "admin.list", admins: [{ id: "814860", type: "admin" }] }, + admin_assignee_id: 814860, + team_assignee_id: "5017691", + open: true, + state: "open", + read: true, + tags: { type: "tag.list", tags: [] }, + priority: "not_priority", + conversation_rating: null, + statistics: { + time_to_assignment: 300, + time_to_admin_reply: 4300, + time_to_first_close: null, + time_to_last_close: null, + median_time_to_reply: 4300, + first_contact_reply_at: 1781524800, + first_assignment_at: 1781525100, + first_admin_reply_at: 1781529100, + first_close_at: null, + last_assignment_at: 1781525100, + last_contact_reply_at: 1781528440, + last_admin_reply_at: 1781529100, + last_close_at: null, + count_reopens: 0, + count_assignments: 1, + count_conversation_parts: 2, + }, + conversation_parts: { + type: "conversation_part.list", + conversation_parts: [ + { + type: "conversation_part", + id: "403982355", + part_type: "comment", + body: "

Thanks for the extra detail, can you confirm which browser you're using?

", + created_at: 1781529100, + updated_at: 1781529100, + notified_at: 1781529100, + assigned_to: null, + author: { + type: "admin", + id: "814860", + name: "Jordan Mills", + email: "jordan@acme.example", + }, + attachments: [], + external_id: null, + redacted: false, + }, + ], + total_count: 2, + }, + }, + }, + }, + docsUrl: "https://developers.intercom.com/docs/references/webhooks/webhook-models", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "intercom", + providerLabel: "Intercom", + eventType: "contact.created", + name: "Contact created", + description: "A new contact was created in the workspace.", + body: { + type: "notification_event", + topic: "contact.created", + id: "notif_a3d81f6e-9c02-4b7a-b1e4-5f8d0c2a6b93", + app_id: "a86dr8yl", + created_at: 1781524800, + delivery_attempts: 1, + first_sent_at: 1781524800, + data: { + item: { + type: "contact", + id: "6620fa1e8f2b3c4d5e6f7890", + workspace_id: "a86dr8yl", + external_id: "cust_48213", + role: "user", + email: "priya@northwind.example", + email_domain: "northwind.example", + phone: null, + name: "Priya Desai", + avatar: null, + owner_id: null, + social_profiles: { type: "list", data: [] }, + has_hard_bounced: false, + marked_email_as_spam: false, + unsubscribed_from_emails: false, + created_at: 1781524800, + updated_at: 1781524800, + signed_up_at: 1781524800, + last_seen_at: 1781524800, + last_replied_at: null, + last_contacted_at: null, + last_email_opened_at: null, + last_email_clicked_at: null, + language_override: null, + browser: "chrome", + browser_version: "126.0.0.0", + browser_language: "en", + os: "Mac OS X", + location: { + type: "location", + country: "United States", + region: "California", + city: "San Francisco", + }, + custom_attributes: {}, + tags: { + type: "list", + data: [], + url: "/contacts/6620fa1e8f2b3c4d5e6f7890/tags", + total_count: 0, + has_more: false, + }, + companies: { + type: "list", + data: [], + url: "/contacts/6620fa1e8f2b3c4d5e6f7890/companies", + total_count: 0, + has_more: false, + }, + }, + }, + }, + docsUrl: "https://developers.intercom.com/docs/references/webhooks/webhook-models", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "intercom", + providerLabel: "Intercom", + eventType: "contact.deleted", + name: "Contact deleted", + description: "A contact was permanently deleted from the workspace.", + body: { + type: "notification_event", + topic: "contact.deleted", + id: "notif_6e2b4d81-7a5f-4c3e-9d0a-1f6b8c2e4a70", + app_id: "a86dr8yl", + created_at: 1781612900, + delivery_attempts: 1, + first_sent_at: 1781612900, + data: { + item: { + type: "contact", + id: "5ba682d23d7cf92bef87bfd4", + external_id: "cust_10248", + deleted: true, + }, + }, + }, + docsUrl: "https://developers.intercom.com/docs/references/webhooks/webhook-models", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/jira.ts b/internal-packages/webhook-sources/src/handAuthored/jira.ts new file mode 100644 index 00000000000..d0a3a5d55e0 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/jira.ts @@ -0,0 +1,235 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Jira Cloud samples. Jira signs webhooks with an `X-Hub-Signature` header (no `-256`), formatted as + * `sha256=` HMAC over the raw body - this does not match our `github` preset (which requires the + * `x-hub-signature-256` header name), so these ship without a `presetId`. + */ +export const samples: SampleRecord[] = [ + { + provider: "jira", + providerLabel: "Jira", + eventType: "jira:issue_created", + name: "Issue created", + description: "A new issue was created in a Jira Cloud project.", + body: { + timestamp: 1784065200000, + webhookEvent: "jira:issue_created", + issue_event_type_name: "issue_created", + user: { + self: "https://example.atlassian.net/rest/api/2/user?accountId=5f8a1b2c3d4e5f0a1b2c3d4e", + accountId: "5f8a1b2c3d4e5f0a1b2c3d4e", + accountType: "atlassian", + displayName: "Jordan Lee", + active: true, + timeZone: "America/New_York", + }, + issue: { + id: "10142", + self: "https://example.atlassian.net/rest/api/2/issue/10142", + key: "ENG-482", + fields: { + summary: "Webhook retries silently drop after 3 attempts", + created: "2026-07-13T09:20:00.000-0400", + issuetype: { id: "10001", name: "Bug" }, + status: { id: "10000", name: "To Do" }, + priority: { id: "3", name: "Medium" }, + project: { id: "10005", key: "ENG", name: "Engineering" }, + labels: ["webhooks", "reliability"], + assignee: null, + reporter: { + accountId: "5f8a1b2c3d4e5f0a1b2c3d4e", + displayName: "Jordan Lee", + }, + }, + }, + }, + docsUrl: "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "jira", + providerLabel: "Jira", + eventType: "jira:issue_updated", + name: "Issue updated", + description: "An issue's status and priority changed; `changelog.items` lists the field diffs.", + body: { + timestamp: 1784068800000, + webhookEvent: "jira:issue_updated", + issue_event_type_name: "issue_generic", + user: { + self: "https://example.atlassian.net/rest/api/2/user?accountId=6a9b2c3d4e5f6a1b2c3d4e5f", + accountId: "6a9b2c3d4e5f6a1b2c3d4e5f", + accountType: "atlassian", + displayName: "Priya Nair", + active: true, + timeZone: "Europe/London", + }, + issue: { + id: "10142", + self: "https://example.atlassian.net/rest/api/2/issue/10142", + key: "ENG-482", + fields: { + summary: "Webhook retries silently drop after 3 attempts", + created: "2026-07-13T09:20:00.000-0400", + issuetype: { id: "10001", name: "Bug" }, + status: { id: "10001", name: "In Progress" }, + priority: { id: "2", name: "High" }, + project: { id: "10005", key: "ENG", name: "Engineering" }, + assignee: { + accountId: "6a9b2c3d4e5f6a1b2c3d4e5f", + displayName: "Priya Nair", + }, + }, + }, + changelog: { + id: "10391", + items: [ + { + field: "status", + fieldtype: "jira", + from: "10000", + fromString: "To Do", + to: "10001", + toString: "In Progress", + }, + { + field: "priority", + fieldtype: "jira", + from: "3", + fromString: "Medium", + to: "2", + toString: "High", + }, + ], + }, + }, + docsUrl: "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "jira", + providerLabel: "Jira", + eventType: "jira:issue_deleted", + name: "Issue deleted", + description: "An issue was permanently deleted from a Jira Cloud project.", + body: { + timestamp: 1784072400000, + webhookEvent: "jira:issue_deleted", + issue_event_type_name: "issue_deleted", + user: { + self: "https://example.atlassian.net/rest/api/2/user?accountId=6a9b2c3d4e5f6a1b2c3d4e5f", + accountId: "6a9b2c3d4e5f6a1b2c3d4e5f", + accountType: "atlassian", + displayName: "Priya Nair", + active: true, + timeZone: "Europe/London", + }, + issue: { + id: "10098", + self: "https://example.atlassian.net/rest/api/2/issue/10098", + key: "ENG-417", + fields: { + summary: "Duplicate of ENG-482", + issuetype: { id: "10001", name: "Bug" }, + status: { id: "10002", name: "Done" }, + project: { id: "10005", key: "ENG", name: "Engineering" }, + }, + }, + }, + docsUrl: "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "jira", + providerLabel: "Jira", + eventType: "comment_created", + name: "Comment created", + description: "A comment was added to an issue.", + body: { + timestamp: 1784076000000, + webhookEvent: "comment_created", + issue_event_type_name: "comment_created", + user: { + self: "https://example.atlassian.net/rest/api/2/user?accountId=7b0c3d4e5f6a1b2c3d4e5f6a", + accountId: "7b0c3d4e5f6a1b2c3d4e5f6a", + accountType: "atlassian", + displayName: "Sam Okafor", + active: true, + timeZone: "Africa/Lagos", + }, + issue: { + id: "10142", + self: "https://example.atlassian.net/rest/api/2/issue/10142", + key: "ENG-482", + fields: { + summary: "Webhook retries silently drop after 3 attempts", + issuetype: { id: "10001", name: "Bug" }, + status: { id: "10001", name: "In Progress" }, + project: { id: "10005", key: "ENG", name: "Engineering" }, + }, + }, + comment: { + self: "https://example.atlassian.net/rest/api/3/issue/ENG-482/comment/48213", + id: "48213", + author: { + accountId: "7b0c3d4e5f6a1b2c3d4e5f6a", + displayName: "Sam Okafor", + }, + body: "I can reproduce this - the retry counter resets on redeploy, not per delivery.", + created: "2026-07-13T11:20:00.000-0400", + updated: "2026-07-13T11:20:00.000-0400", + }, + }, + docsUrl: "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "jira", + providerLabel: "Jira", + eventType: "comment_updated", + name: "Comment updated", + description: "An existing comment was edited; `updateAuthor` and `updated` reflect the edit.", + body: { + timestamp: 1784079600000, + webhookEvent: "comment_updated", + issue_event_type_name: "comment_updated", + user: { + self: "https://example.atlassian.net/rest/api/2/user?accountId=7b0c3d4e5f6a1b2c3d4e5f6a", + accountId: "7b0c3d4e5f6a1b2c3d4e5f6a", + accountType: "atlassian", + displayName: "Sam Okafor", + active: true, + timeZone: "Africa/Lagos", + }, + issue: { + id: "10142", + self: "https://example.atlassian.net/rest/api/2/issue/10142", + key: "ENG-482", + fields: { + summary: "Webhook retries silently drop after 3 attempts", + issuetype: { id: "10001", name: "Bug" }, + status: { id: "10001", name: "In Progress" }, + project: { id: "10005", key: "ENG", name: "Engineering" }, + }, + }, + comment: { + self: "https://example.atlassian.net/rest/api/3/issue/ENG-482/comment/48213", + id: "48213", + author: { + accountId: "7b0c3d4e5f6a1b2c3d4e5f6a", + displayName: "Sam Okafor", + }, + updateAuthor: { + accountId: "7b0c3d4e5f6a1b2c3d4e5f6a", + displayName: "Sam Okafor", + }, + body: "I can reproduce this - the retry counter resets on redeploy, not per delivery. Filed as ENG-483.", + created: "2026-07-13T11:20:00.000-0400", + updated: "2026-07-13T12:05:00.000-0400", + }, + }, + docsUrl: "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/linear.ts b/internal-packages/webhook-sources/src/handAuthored/linear.ts new file mode 100644 index 00000000000..4ecc68af6e5 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/linear.ts @@ -0,0 +1,197 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Linear samples. Linear signs webhooks with a custom `Linear-Signature` HMAC-SHA256 scheme (not one + * of our presets), so no `presetId` is set on any sample here. `eventType` joins the body's top-level + * `type` and `action` fields (e.g. "Issue.create") since `type` alone ("Issue") doesn't distinguish + * create/update/remove. + */ +export const samples: SampleRecord[] = [ + { + provider: "linear", + providerLabel: "Linear", + eventType: "Issue.create", + name: "Issue created", + description: "A new issue was created on a team.", + body: { + action: "create", + type: "Issue", + createdAt: "2026-07-01T14:32:18.084Z", + webhookTimestamp: 1751380338084, + webhookId: "3e9c1b2a-8f47-4c6d-9a1e-7b5d2c4f8e91", + organizationId: "b6a1d4e2-9c3f-4a7b-8e2d-1f6c9a4b7d3e", + actor: { + id: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + type: "user", + name: "Priya Nair", + email: "priya@acme.dev", + }, + data: { + id: "7c2e4f1a-9b3d-4a6e-8c5f-1d7b3e9a2c4f", + identifier: "ENG-483", + number: 483, + title: "Webhook signature verification fails for rotated secrets", + description: + "When a signing secret is rotated mid-flight, in-progress deliveries fail verification.", + priority: 2, + priorityLabel: "High", + estimate: 3, + stateId: "1a9d3f7c-5b2e-4d8a-9c6f-3e7b1a4d9c2f", + assigneeId: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + teamId: "5e8b2d4a-7c1f-4a9e-8d3b-6f2c9a5e7d1b", + projectId: "9f3a7c5e-1d8b-4f6a-9c2e-7b5d3a8f1c6e", + labelIds: ["2c6e9b4d-8a1f-4c7e-9d3b-5a8f2c6e9b4d"], + subscriberIds: ["d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f"], + createdAt: "2026-07-01T14:32:18.076Z", + updatedAt: "2026-07-01T14:32:18.076Z", + archivedAt: null, + }, + url: "https://linear.app/acme/issue/ENG-483/webhook-signature-verification-fails-for-rotated-secrets", + }, + docsUrl: "https://linear.app/developers/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "linear", + providerLabel: "Linear", + eventType: "Issue.update", + name: "Issue updated", + description: "An issue moved state and was reassigned; `updatedFrom` carries the prior values.", + body: { + action: "update", + type: "Issue", + createdAt: "2026-07-01T16:45:22.391Z", + webhookTimestamp: 1751388322391, + webhookId: "3e9c1b2a-8f47-4c6d-9a1e-7b5d2c4f8e91", + organizationId: "b6a1d4e2-9c3f-4a7b-8e2d-1f6c9a4b7d3e", + actor: { + id: "6b1e9d3a-4c7f-4b2e-8a9d-1c6f3b8e5a2d", + type: "user", + name: "Marcus Webb", + email: "marcus@acme.dev", + }, + data: { + id: "7c2e4f1a-9b3d-4a6e-8c5f-1d7b3e9a2c4f", + identifier: "ENG-483", + number: 483, + stateId: "4d8a2f6c-9b1e-4d7a-8c3f-6e2b9d4a7c1f", + assigneeId: "6b1e9d3a-4c7f-4b2e-8a9d-1c6f3b8e5a2d", + startedAt: "2026-07-01T16:45:22.384Z", + completedAt: null, + updatedAt: "2026-07-01T16:45:22.391Z", + }, + updatedFrom: { + stateId: "1a9d3f7c-5b2e-4d8a-9c6f-3e7b1a4d9c2f", + assigneeId: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + startedAt: null, + updatedAt: "2026-07-01T14:32:18.076Z", + }, + url: "https://linear.app/acme/issue/ENG-483/webhook-signature-verification-fails-for-rotated-secrets", + }, + docsUrl: "https://linear.app/developers/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "linear", + providerLabel: "Linear", + eventType: "Comment.create", + name: "Comment created", + description: "A new comment was posted on an issue.", + body: { + action: "create", + type: "Comment", + actor: { + id: "6b1e9d3a-4c7f-4b2e-8a9d-1c6f3b8e5a2d", + type: "user", + name: "Marcus Webb", + email: "marcus@acme.dev", + url: "https://linear.app/acme/profiles/marcus", + }, + data: { + id: "8d4f2a6c-1b9e-4d7a-8c3f-6e2b9d4a7c1f", + createdAt: "2026-07-01T17:02:09.514Z", + updatedAt: "2026-07-01T17:02:09.514Z", + archivedAt: null, + body: "I've reproduced this against the staging secret rotation, fix incoming.", + edited: false, + issueId: "7c2e4f1a-9b3d-4a6e-8c5f-1d7b3e9a2c4f", + userId: "6b1e9d3a-4c7f-4b2e-8a9d-1c6f3b8e5a2d", + }, + url: "https://linear.app/acme/issue/ENG-483/webhook-signature-verification-fails-for-rotated-secrets#comment-8d4f2a6c-1b9e-4d7a-8c3f-6e2b9d4a7c1f", + createdAt: "2026-07-01T17:02:09.514Z", + organizationId: "b6a1d4e2-9c3f-4a7b-8e2d-1f6c9a4b7d3e", + webhookTimestamp: 1751389329514, + webhookId: "3e9c1b2a-8f47-4c6d-9a1e-7b5d2c4f8e91", + }, + docsUrl: "https://linear.app/developers/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "linear", + providerLabel: "Linear", + eventType: "IssueLabel.create", + name: "Issue label created", + description: "A new team label was created.", + body: { + action: "create", + type: "IssueLabel", + createdAt: "2026-07-01T09:15:44.201Z", + webhookTimestamp: 1751361344201, + webhookId: "3e9c1b2a-8f47-4c6d-9a1e-7b5d2c4f8e91", + organizationId: "b6a1d4e2-9c3f-4a7b-8e2d-1f6c9a4b7d3e", + actor: { + id: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + type: "user", + name: "Priya Nair", + email: "priya@acme.dev", + }, + data: { + id: "2c6e9b4d-8a1f-4c7e-9d3b-5a8f2c6e9b4d", + name: "webhooks", + color: "#26b5ce", + description: "Work related to inbound/outbound webhook delivery.", + teamId: "5e8b2d4a-7c1f-4a9e-8d3b-6f2c9a5e7d1b", + creatorId: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + createdAt: "2026-07-01T09:15:44.192Z", + updatedAt: "2026-07-01T09:15:44.192Z", + archivedAt: null, + }, + url: "https://linear.app/acme/team/ENG/labels", + }, + docsUrl: "https://linear.app/developers/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "linear", + providerLabel: "Linear", + eventType: "Reaction.create", + name: "Reaction created", + description: "An emoji reaction was added to a comment.", + body: { + action: "create", + type: "Reaction", + createdAt: "2026-07-01T17:05:33.802Z", + webhookTimestamp: 1751389533802, + webhookId: "3e9c1b2a-8f47-4c6d-9a1e-7b5d2c4f8e91", + organizationId: "b6a1d4e2-9c3f-4a7b-8e2d-1f6c9a4b7d3e", + actor: { + id: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + type: "user", + name: "Priya Nair", + email: "priya@acme.dev", + }, + data: { + id: "9e1c4f7a-2b8d-4e6c-9a3f-7d5b2e8c1a4f", + emoji: "🚀", + commentId: "8d4f2a6c-1b9e-4d7a-8c3f-6e2b9d4a7c1f", + userId: "d4f8a2c1-6e9b-4d3a-8f1c-2b7e5a9d4c6f", + createdAt: "2026-07-01T17:05:33.792Z", + updatedAt: "2026-07-01T17:05:33.792Z", + archivedAt: null, + }, + url: "https://linear.app/acme/issue/ENG-483/webhook-signature-verification-fails-for-rotated-secrets#comment-8d4f2a6c-1b9e-4d7a-8c3f-6e2b9d4a7c1f", + }, + docsUrl: "https://linear.app/developers/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/notion.ts b/internal-packages/webhook-sources/src/handAuthored/notion.ts new file mode 100644 index 00000000000..dbf2e033158 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/notion.ts @@ -0,0 +1,136 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Notion samples. No preset: Notion signs with `X-Notion-Signature: sha256=`, an HMAC-SHA256 of + * the raw JSON body keyed by the subscription's `verification_token`. The header name and lack of a + * `t=`/`v1=` component don't match our stripe, github, svix, square, or discord presets. + */ +export const samples: SampleRecord[] = [ + { + provider: "notion", + providerLabel: "Notion", + eventType: "page.created", + name: "Page created (in database)", + description: "A new page was added to a database the integration is subscribed to.", + body: { + id: "6b6f6b8a-8e0d-4f2a-9f3d-2a6b1c4e7d9f", + timestamp: "2026-06-30T14:02:11.000Z", + workspace_id: "13950b26-c203-4f3b-b97d-93ec06319565", + workspace_name: "Acme Product", + subscription_id: "29d75c0d-5546-4414-8459-7b7a92f1fc4b", + integration_id: "0ef2e755-4912-8096-91c1-00376a88a5ca", + type: "page.created", + authors: [{ id: "c7c11cca-1d73-471d-9b6e-bdef51470190", type: "person" }], + attempt_number: 1, + entity: { id: "0ef104cd-477e-80e1-8571-cfd10e92339a", type: "page" }, + data: { + parent: { id: "3d8b9a2c-5e1f-4a6d-8c3b-7e9f2d5a1b6c", type: "database" }, + }, + }, + docsUrl: "https://developers.notion.com/reference/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "notion", + providerLabel: "Notion", + eventType: "page.content_updated", + name: "Page content updated (block added)", + description: + "A block was added to a page's content; delivery is aggregated over a short window.", + body: { + id: "56c3e00c-4f0c-4566-9676-4b058a50a03d", + timestamp: "2026-07-01T19:49:36.997Z", + workspace_id: "13950b26-c203-4f3b-b97d-93ec06319565", + workspace_name: "Acme Product", + subscription_id: "29d75c0d-5546-4414-8459-7b7a92f1fc4b", + integration_id: "0ef2e755-4912-8096-91c1-00376a88a5ca", + type: "page.content_updated", + authors: [{ id: "c7c11cca-1d73-471d-9b6e-bdef51470190", type: "person" }], + attempt_number: 1, + entity: { id: "0ef104cd-477e-80e1-8571-cfd10e92339a", type: "page" }, + data: { + updated_blocks: [{ id: "153104cd-477e-80ec-a87d-f7ff0236d35c", type: "block" }], + parent: { id: "0ef104cd-477e-80e1-8571-cfd10e92339a", type: "page" }, + }, + }, + docsUrl: "https://developers.notion.com/reference/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "notion", + providerLabel: "Notion", + eventType: "page.properties_updated", + name: "Page properties updated (status changed)", + description: "One or more database properties on a page changed value, e.g. a status field.", + body: { + id: "8a4d1c7e-2b5f-4d9a-9e3c-6f0a8b2d5c1e", + timestamp: "2026-07-02T09:15:47.000Z", + workspace_id: "13950b26-c203-4f3b-b97d-93ec06319565", + workspace_name: "Acme Product", + subscription_id: "29d75c0d-5546-4414-8459-7b7a92f1fc4b", + integration_id: "0ef2e755-4912-8096-91c1-00376a88a5ca", + type: "page.properties_updated", + authors: [{ id: "c7c11cca-1d73-471d-9b6e-bdef51470190", type: "person" }], + attempt_number: 1, + entity: { id: "5e1f9a3c-6d2b-4f8e-8a1c-3b6d9f2e5a8c", type: "page" }, + data: { + parent: { id: "3d8b9a2c-5e1f-4a6d-8c3b-7e9f2d5a1b6c", type: "database" }, + updated_properties: ["a1b2%3Ac4d5", "Status"], + }, + }, + docsUrl: "https://developers.notion.com/reference/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "notion", + providerLabel: "Notion", + eventType: "database.content_updated", + name: "Database content updated", + description: + "A page belonging to the database changed. Deprecated in favor of data_source.content_updated as of Notion API version 2025-09-03, but still emitted for integrations pinned to older versions.", + body: { + id: "2f5a8d1c-7e3b-4a6f-9c2d-8b1e5a3f7c9d", + timestamp: "2026-07-03T11:32:05.000Z", + workspace_id: "13950b26-c203-4f3b-b97d-93ec06319565", + workspace_name: "Acme Product", + subscription_id: "29d75c0d-5546-4414-8459-7b7a92f1fc4b", + integration_id: "0ef2e755-4912-8096-91c1-00376a88a5ca", + type: "database.content_updated", + authors: [{ id: "c7c11cca-1d73-471d-9b6e-bdef51470190", type: "person" }], + attempt_number: 1, + entity: { id: "3d8b9a2c-5e1f-4a6d-8c3b-7e9f2d5a1b6c", type: "database" }, + data: { + updated_blocks: [{ id: "9f2d5a8c-1e6b-4d3f-9a7c-2b5e8d1f4a6c", type: "block" }], + parent: { id: "7c1e4b9d-2a6f-4e8c-9d3b-5f1a8c2e6d9b", type: "workspace" }, + }, + }, + docsUrl: "https://developers.notion.com/reference/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "notion", + providerLabel: "Notion", + eventType: "comment.created", + name: "Comment created (on a page)", + description: + "A new top-level comment was added to a page; requires the integration's comment-read capability.", + body: { + id: "4b7e1a9d-6c3f-4e8b-8a2d-9f1c5b7e3a6d", + timestamp: "2026-07-05T16:20:59.000Z", + workspace_id: "13950b26-c203-4f3b-b97d-93ec06319565", + workspace_name: "Acme Product", + subscription_id: "29d75c0d-5546-4414-8459-7b7a92f1fc4b", + integration_id: "0ef2e755-4912-8096-91c1-00376a88a5ca", + type: "comment.created", + authors: [{ id: "1e6b9c3f-8d2a-4c7e-9b1d-6a3f8c2e5d9b", type: "person" }], + attempt_number: 1, + entity: { id: "6d9c2a5e-7f1b-4a3d-9e6c-2b5f8a1d4c7e", type: "comment" }, + data: { + page_id: "0ef104cd-477e-80e1-8571-cfd10e92339a", + parent: { id: "0ef104cd-477e-80e1-8571-cfd10e92339a", type: "page" }, + }, + }, + docsUrl: "https://developers.notion.com/reference/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/openai.ts b/internal-packages/webhook-sources/src/handAuthored/openai.ts new file mode 100644 index 00000000000..a2d31be8d2f --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/openai.ts @@ -0,0 +1,135 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * OpenAI samples. OpenAI signs webhooks with Svix, so `presetId: "svix"` keeps them under the + * round-trip guarantee. Every event is a thin envelope (`id`, `object: "event"`, `created_at`, + * `type`, `data`) where `data` carries only the resource id (or, for the Realtime SIP event, the + * call id + SIP headers) - integrators call back the relevant API with that id to fetch full state. + */ +export const samples: SampleRecord[] = [ + { + provider: "openai", + providerLabel: "OpenAI", + presetId: "svix", + eventType: "batch.completed", + name: "Batch completed", + description: + "An async Batch API job finished successfully. Fetch the batch with `data.id` to get the output file id.", + body: { + id: "evt_685343a1381c819085d44c354e1b330e", + object: "event", + created_at: 1784065200, + type: "batch.completed", + data: { + id: "batch_67d1e3d640848190a3f3f19c74e9d9de", + }, + }, + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "openai", + providerLabel: "OpenAI", + presetId: "svix", + eventType: "batch.failed", + name: "Batch failed", + description: + "An async Batch API job failed. Fetch the batch with `data.id` for the error file and failure reason.", + body: { + id: "evt_5f2c9a7e6d1b4c3a9e0f7d2b8c6a1e4f", + object: "event", + created_at: 1784068800, + type: "batch.failed", + data: { + id: "batch_a1b2c3d4e5f647890a1b2c3d4e5f6789", + }, + }, + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "openai", + providerLabel: "OpenAI", + presetId: "svix", + eventType: "fine_tuning.job.succeeded", + name: "Fine-tuning job succeeded", + description: + "A fine-tuning job finished training. Fetch the job with `data.id` to get the fine-tuned model name.", + body: { + id: "evt_3d8e1f6a9c2b4d7e0f5a8c1b6d9e2f4a", + object: "event", + created_at: 1784072400, + type: "fine_tuning.job.succeeded", + data: { + id: "ftjob-3sPygNaFf0EBAJoATNqFrOca", + }, + }, + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "openai", + providerLabel: "OpenAI", + presetId: "svix", + eventType: "response.completed", + name: "Response completed", + description: + "A background Responses API request finished. Fetch it with `data.id` to get the full output.", + body: { + id: "evt_685343a1381c819085d44c354e1b330e", + object: "event", + created_at: 1784076000, + type: "response.completed", + data: { + id: "resp_67ccd6a5da548190a97a9ff8b48e1d6c", + }, + }, + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "openai", + providerLabel: "OpenAI", + presetId: "svix", + eventType: "eval.run.succeeded", + name: "Eval run succeeded", + description: + "An Evals API run finished. Fetch it with `data.id` to get result counts and per-model usage.", + body: { + id: "evt_9b4d7f2a5c8e1b3d6f9a2c5e8b1d4f7a", + object: "event", + created_at: 1784079600, + type: "eval.run.succeeded", + data: { + id: "evalrun_67abd54d60ec8190832b46859da808f7", + }, + }, + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "openai", + providerLabel: "OpenAI", + presetId: "svix", + eventType: "realtime.call.incoming", + name: "Incoming Realtime SIP call", + description: + "A SIP call arrived for the Realtime API. Use `data.call_id` to accept or reject it before it rings out.", + body: { + id: "evt_685343a1381c819085d44c354e1b330e", + object: "event", + created_at: 1784083200, + type: "realtime.call.incoming", + data: { + call_id: "call_C9RyJt3H0IXXCCUmr1cxoQZ5aB2", + sip_headers: [ + { name: "From", value: "sip:+14255551212@sip.example.com" }, + { name: "To", value: "sip:+18005551212@sip.example.com" }, + { name: "Call-ID", value: "03782086-4ce9-44bf-8b0d-4e303d2cc590" }, + ], + }, + }, + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/pagerduty.ts b/internal-packages/webhook-sources/src/handAuthored/pagerduty.ts new file mode 100644 index 00000000000..e1ac5c0f7a6 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/pagerduty.ts @@ -0,0 +1,355 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * PagerDuty v3 webhook samples. Every delivery wraps a single `event` envelope + * (`id`, `event_type`, `resource_type`, `occurred_at`, `agent`, `client`, `data`); `data` is the full + * incident object at the moment of the event. `eventType` here matches `event.event_type`, the real + * discriminant. No `presetId` is set (PagerDuty's `X-PagerDuty-Signature: v1=` is a bespoke + * HMAC-SHA256 over the raw body with no matching preset). + */ +export const samples: SampleRecord[] = [ + { + provider: "pagerduty", + providerLabel: "PagerDuty", + eventType: "incident.triggered", + name: "Incident triggered", + description: "A monitoring integration opened a new incident via the Events API.", + body: { + event: { + id: "01J2Y3ZK9M4Q7R8T2VBN5X0F1A", + event_type: "incident.triggered", + resource_type: "incident", + occurred_at: "2026-07-14T02:11:03Z", + agent: null, + client: null, + data: { + id: "PT4KHXT", + type: "incident", + self: "https://api.pagerduty.com/incidents/PT4KHXT", + html_url: "https://acme-corp.pagerduty.com/incidents/PT4KHXT", + number: 4821, + status: "triggered", + incident_key: "prod-api-5xx-spike", + created_at: "2026-07-14T02:11:03Z", + title: "Elevated 5xx error rate on prod-api-gateway", + service: { + id: "PSVCACME1", + type: "service_reference", + summary: "prod-api-gateway", + self: "https://api.pagerduty.com/services/PSVCACME1", + html_url: "https://acme-corp.pagerduty.com/services/PSVCACME1", + }, + assignees: [ + { + id: "PUSRJDOE1", + type: "user_reference", + summary: "Jamie Chen", + self: "https://api.pagerduty.com/users/PUSRJDOE1", + html_url: "https://acme-corp.pagerduty.com/users/PUSRJDOE1", + }, + ], + escalation_policy: { + id: "PEPACME01", + type: "escalation_policy_reference", + summary: "Platform On-Call", + self: "https://api.pagerduty.com/escalation_policies/PEPACME01", + html_url: "https://acme-corp.pagerduty.com/escalation_policies/PEPACME01", + }, + teams: [], + priority: { + id: "PRIP1XXX", + type: "priority_reference", + summary: "P2", + self: "https://api.pagerduty.com/priorities/PRIP1XXX", + html_url: "https://acme-corp.pagerduty.com/account/incident_priorities", + }, + urgency: "high", + conference_bridge: null, + resolve_reason: null, + }, + }, + }, + docsUrl: "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "pagerduty", + providerLabel: "PagerDuty", + eventType: "incident.acknowledged", + name: "Incident acknowledged", + description: "The on-call responder acknowledged the incident.", + body: { + event: { + id: "01J2Y40D3H6K8N1P5WCM7Y2G3B", + event_type: "incident.acknowledged", + resource_type: "incident", + occurred_at: "2026-07-14T02:13:47Z", + agent: { + id: "PUSRJDOE1", + type: "user_reference", + summary: "Jamie Chen", + self: "https://api.pagerduty.com/users/PUSRJDOE1", + html_url: "https://acme-corp.pagerduty.com/users/PUSRJDOE1", + }, + client: null, + data: { + id: "PT4KHXT", + type: "incident", + self: "https://api.pagerduty.com/incidents/PT4KHXT", + html_url: "https://acme-corp.pagerduty.com/incidents/PT4KHXT", + number: 4821, + status: "acknowledged", + incident_key: "prod-api-5xx-spike", + created_at: "2026-07-14T02:11:03Z", + title: "Elevated 5xx error rate on prod-api-gateway", + service: { + id: "PSVCACME1", + type: "service_reference", + summary: "prod-api-gateway", + self: "https://api.pagerduty.com/services/PSVCACME1", + html_url: "https://acme-corp.pagerduty.com/services/PSVCACME1", + }, + assignees: [ + { + id: "PUSRJDOE1", + type: "user_reference", + summary: "Jamie Chen", + self: "https://api.pagerduty.com/users/PUSRJDOE1", + html_url: "https://acme-corp.pagerduty.com/users/PUSRJDOE1", + }, + ], + acknowledgements: [ + { + at: "2026-07-14T02:13:47Z", + acknowledger: { + id: "PUSRJDOE1", + type: "user_reference", + summary: "Jamie Chen", + self: "https://api.pagerduty.com/users/PUSRJDOE1", + html_url: "https://acme-corp.pagerduty.com/users/PUSRJDOE1", + }, + }, + ], + escalation_policy: { + id: "PEPACME01", + type: "escalation_policy_reference", + summary: "Platform On-Call", + self: "https://api.pagerduty.com/escalation_policies/PEPACME01", + html_url: "https://acme-corp.pagerduty.com/escalation_policies/PEPACME01", + }, + teams: [], + priority: { + id: "PRIP1XXX", + type: "priority_reference", + summary: "P2", + self: "https://api.pagerduty.com/priorities/PRIP1XXX", + html_url: "https://acme-corp.pagerduty.com/account/incident_priorities", + }, + urgency: "high", + conference_bridge: null, + last_status_change_at: "2026-07-14T02:13:47Z", + }, + }, + }, + docsUrl: "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "pagerduty", + providerLabel: "PagerDuty", + eventType: "incident.escalated", + name: "Incident escalated", + description: "Acknowledgement timed out and the incident escalated to the next on-call level.", + body: { + event: { + id: "01J2Y41V7Q3W9M2N6XDP8Z4H5C", + event_type: "incident.escalated", + resource_type: "incident", + occurred_at: "2026-07-14T02:23:47Z", + agent: null, + client: null, + data: { + id: "PT4KHXT", + type: "incident", + self: "https://api.pagerduty.com/incidents/PT4KHXT", + html_url: "https://acme-corp.pagerduty.com/incidents/PT4KHXT", + number: 4821, + status: "triggered", + incident_key: "prod-api-5xx-spike", + created_at: "2026-07-14T02:11:03Z", + title: "Elevated 5xx error rate on prod-api-gateway", + service: { + id: "PSVCACME1", + type: "service_reference", + summary: "prod-api-gateway", + self: "https://api.pagerduty.com/services/PSVCACME1", + html_url: "https://acme-corp.pagerduty.com/services/PSVCACME1", + }, + assignees: [ + { + id: "PUSRMRYS2", + type: "user_reference", + summary: "Morgan Reyes", + self: "https://api.pagerduty.com/users/PUSRMRYS2", + html_url: "https://acme-corp.pagerduty.com/users/PUSRMRYS2", + }, + ], + escalation_policy: { + id: "PEPACME01", + type: "escalation_policy_reference", + summary: "Platform On-Call", + self: "https://api.pagerduty.com/escalation_policies/PEPACME01", + html_url: "https://acme-corp.pagerduty.com/escalation_policies/PEPACME01", + }, + teams: [], + priority: { + id: "PRIP1XXX", + type: "priority_reference", + summary: "P2", + self: "https://api.pagerduty.com/priorities/PRIP1XXX", + html_url: "https://acme-corp.pagerduty.com/account/incident_priorities", + }, + urgency: "high", + conference_bridge: null, + last_status_change_at: "2026-07-14T02:23:47Z", + }, + }, + }, + docsUrl: "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "pagerduty", + providerLabel: "PagerDuty", + eventType: "incident.reassigned", + name: "Incident reassigned", + description: "The first responder reassigned the incident to a teammate.", + body: { + event: { + id: "01J2Y42F5T8V1M3P7YCQ9A6J7D", + event_type: "incident.reassigned", + resource_type: "incident", + occurred_at: "2026-07-14T02:25:10Z", + agent: { + id: "PUSRJDOE1", + type: "user_reference", + summary: "Jamie Chen", + self: "https://api.pagerduty.com/users/PUSRJDOE1", + html_url: "https://acme-corp.pagerduty.com/users/PUSRJDOE1", + }, + client: null, + data: { + id: "PT4KHXT", + type: "incident", + self: "https://api.pagerduty.com/incidents/PT4KHXT", + html_url: "https://acme-corp.pagerduty.com/incidents/PT4KHXT", + number: 4821, + status: "triggered", + incident_key: "prod-api-5xx-spike", + created_at: "2026-07-14T02:11:03Z", + title: "Elevated 5xx error rate on prod-api-gateway", + service: { + id: "PSVCACME1", + type: "service_reference", + summary: "prod-api-gateway", + self: "https://api.pagerduty.com/services/PSVCACME1", + html_url: "https://acme-corp.pagerduty.com/services/PSVCACME1", + }, + assignees: [ + { + id: "PUSRMRYS2", + type: "user_reference", + summary: "Morgan Reyes", + self: "https://api.pagerduty.com/users/PUSRMRYS2", + html_url: "https://acme-corp.pagerduty.com/users/PUSRMRYS2", + }, + ], + escalation_policy: { + id: "PEPACME01", + type: "escalation_policy_reference", + summary: "Platform On-Call", + self: "https://api.pagerduty.com/escalation_policies/PEPACME01", + html_url: "https://acme-corp.pagerduty.com/escalation_policies/PEPACME01", + }, + teams: [], + priority: { + id: "PRIP1XXX", + type: "priority_reference", + summary: "P2", + self: "https://api.pagerduty.com/priorities/PRIP1XXX", + html_url: "https://acme-corp.pagerduty.com/account/incident_priorities", + }, + urgency: "high", + conference_bridge: null, + last_status_change_at: "2026-07-14T02:25:10Z", + }, + }, + }, + docsUrl: "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "pagerduty", + providerLabel: "PagerDuty", + eventType: "incident.resolved", + name: "Incident resolved", + description: "The responder resolved the incident after rolling back the bad deploy.", + body: { + event: { + id: "01J2Y43R9W2Y4N6Q8ZDS1B7K9E", + event_type: "incident.resolved", + resource_type: "incident", + occurred_at: "2026-07-14T02:41:52Z", + agent: { + id: "PUSRMRYS2", + type: "user_reference", + summary: "Morgan Reyes", + self: "https://api.pagerduty.com/users/PUSRMRYS2", + html_url: "https://acme-corp.pagerduty.com/users/PUSRMRYS2", + }, + client: null, + data: { + id: "PT4KHXT", + type: "incident", + self: "https://api.pagerduty.com/incidents/PT4KHXT", + html_url: "https://acme-corp.pagerduty.com/incidents/PT4KHXT", + number: 4821, + status: "resolved", + incident_key: "prod-api-5xx-spike", + created_at: "2026-07-14T02:11:03Z", + title: "Elevated 5xx error rate on prod-api-gateway", + service: { + id: "PSVCACME1", + type: "service_reference", + summary: "prod-api-gateway", + self: "https://api.pagerduty.com/services/PSVCACME1", + html_url: "https://acme-corp.pagerduty.com/services/PSVCACME1", + }, + assignees: [], + escalation_policy: { + id: "PEPACME01", + type: "escalation_policy_reference", + summary: "Platform On-Call", + self: "https://api.pagerduty.com/escalation_policies/PEPACME01", + html_url: "https://acme-corp.pagerduty.com/escalation_policies/PEPACME01", + }, + teams: [], + priority: { + id: "PRIP1XXX", + type: "priority_reference", + summary: "P2", + self: "https://api.pagerduty.com/priorities/PRIP1XXX", + html_url: "https://acme-corp.pagerduty.com/account/incident_priorities", + }, + urgency: "high", + conference_bridge: null, + resolve_reason: null, + resolved_at: "2026-07-14T02:41:52Z", + last_status_change_at: "2026-07-14T02:41:52Z", + }, + }, + }, + docsUrl: "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/plaid.ts b/internal-packages/webhook-sources/src/handAuthored/plaid.ts new file mode 100644 index 00000000000..eb534bafe9d --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/plaid.ts @@ -0,0 +1,121 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Plaid samples. No `presetId` on any sample: Plaid's `Plaid-Verification` scheme is a JWT (ES256) + * over a body hash, verified against a key fetched from `/webhook_verification_key/get`, not one of + * our HMAC-shared-secret presets. `eventType` joins `webhook_type` and `webhook_code` + * (e.g. "TRANSACTIONS.SYNC_UPDATES_AVAILABLE") to match `eventTypeSource`'s coarser `webhook_type` + * pointer. Plaid's real API delivers "login required" as `webhook_code: "ERROR"` with a nested + * `error.error_code: "ITEM_LOGIN_REQUIRED"` - there is no distinct top-level `LOGIN_REQUIRED` + * webhook_code - so that sample's `eventType` further appends the nested error code to stay + * distinguishable from the other, unrelated ITEM ERROR sample below. + */ +export const samples: SampleRecord[] = [ + { + provider: "plaid", + providerLabel: "Plaid", + eventType: "TRANSACTIONS.SYNC_UPDATES_AVAILABLE", + name: "Transactions sync update available", + description: + "Fires after `/transactions/sync` has new changes to fetch. `initial_update_complete` flips true once the first 30 days are ready; `historical_update_complete` flips true once up to 24 months of history has backfilled.", + body: { + webhook_type: "TRANSACTIONS", + webhook_code: "SYNC_UPDATES_AVAILABLE", + item_id: "gY6VmvpXQPfBn8DjJ4koSA5rZ1MezyCWQ98po", + initial_update_complete: true, + historical_update_complete: false, + environment: "production", + }, + docsUrl: "https://plaid.com/docs/api/products/transactions/#sync_updates_available", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "plaid", + providerLabel: "Plaid", + eventType: "TRANSACTIONS.TRANSACTIONS_REMOVED", + name: "Transactions removed", + description: + "Fires when previously-sent transactions are no longer valid, most commonly when a pending transaction is canceled rather than posting. `removed_transactions` lists the affected transaction ids.", + body: { + webhook_type: "TRANSACTIONS", + webhook_code: "TRANSACTIONS_REMOVED", + removed_transactions: [ + "a8N7bQeWnDx3fKpL9tYoI2vEcJmZ5RsAugTHb", + "j3F1mXsPqRkV6bYtNc8oLwZdEgA0hUiCsxKMe", + ], + item_id: "gY6VmvpXQPfBn8DjJ4koSA5rZ1MezyCWQ98po", + environment: "production", + }, + docsUrl: "https://plaid.com/docs/transactions/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "plaid", + providerLabel: "Plaid", + eventType: "ITEM.ERROR", + name: "Item error: institution not responding", + description: + "A generic Item-level error unrelated to the user's credentials; the institution's servers stopped responding to requests for this Item.", + body: { + webhook_type: "ITEM", + webhook_code: "ERROR", + item_id: "gY6VmvpXQPfBn8DjJ4koSA5rZ1MezyCWQ98po", + user_id: "usr_4hT9dNwXqYb2Lc7fVoAsJk1rGz6PmEuS", + error: { + error_type: "ITEM_ERROR", + error_code: "INSTITUTION_NOT_RESPONDING", + error_message: "the institution is not responding to requests for this item", + display_message: + "We're having trouble connecting to your bank's servers right now. We're working with them to fix it.", + status: 400, + }, + environment: "production", + }, + docsUrl: "https://plaid.com/docs/api/items/#item-error-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "plaid", + providerLabel: "Plaid", + eventType: "ITEM.ERROR.ITEM_LOGIN_REQUIRED", + name: "Item error: login required", + description: + "The user's credentials at the institution changed (password rotation, revoked OAuth consent, etc.) and Link's update mode is needed to restore the Item. Delivered as `webhook_code: ERROR` with `error.error_code: ITEM_LOGIN_REQUIRED` - Plaid has no separate top-level `LOGIN_REQUIRED` webhook_code.", + body: { + webhook_type: "ITEM", + webhook_code: "ERROR", + item_id: "gY6VmvpXQPfBn8DjJ4koSA5rZ1MezyCWQ98po", + user_id: "usr_4hT9dNwXqYb2Lc7fVoAsJk1rGz6PmEuS", + error: { + error_type: "ITEM_ERROR", + error_code: "ITEM_LOGIN_REQUIRED", + error_code_reason: "OAUTH_INVALID_TOKEN", + error_message: + "the login details of this item have changed (credentials, MFA, or required user action) and a user login is required to update this information. use Link's update mode to restore the item to a good state", + display_message: "The user's OAuth connection to this institution has been invalidated.", + status: 400, + }, + environment: "production", + }, + docsUrl: "https://plaid.com/docs/api/items/#item-error-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "plaid", + providerLabel: "Plaid", + eventType: "ITEM.PENDING_EXPIRATION", + name: "Item pending expiration", + description: + "Warns that the user's OAuth consent to the institution will expire soon; `consent_expiration_time` (ISO 8601) is when access is cut off unless the user re-authenticates via update mode.", + body: { + webhook_type: "ITEM", + webhook_code: "PENDING_EXPIRATION", + item_id: "gY6VmvpXQPfBn8DjJ4koSA5rZ1MezyCWQ98po", + user_id: "usr_4hT9dNwXqYb2Lc7fVoAsJk1rGz6PmEuS", + consent_expiration_time: "2026-08-01T13:25:17.000Z", + environment: "production", + }, + docsUrl: "https://plaid.com/docs/api/items/#item-pending_expiration-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/postmark.ts b/internal-packages/webhook-sources/src/handAuthored/postmark.ts new file mode 100644 index 00000000000..e78f6ce4d76 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/postmark.ts @@ -0,0 +1,192 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Postmark samples. Postmark has no HMAC signature scheme - webhook URLs are secured with HTTP Basic + * Auth credentials embedded in the URL, so no `presetId` is set on any sample here. The Delivery, Bounce, + * SpamComplaint, and Open event webhooks each carry a real `RecordType` field matching the registry's + * `eventTypeSource`. The Inbound webhook is a separate, older feature whose documented payload has no + * `RecordType` (or any other discriminant) field at all; its `eventType` below is a synthetic label kept + * for picker/grouping purposes, not a value read from the body. + */ +export const samples: SampleRecord[] = [ + { + provider: "postmark", + providerLabel: "Postmark", + eventType: "Delivery", + name: "Delivery", + description: "A message was successfully delivered to the recipient's mail server.", + body: { + RecordType: "Delivery", + MessageStream: "outbound", + MessageID: "883953f4-6105-42a2-a16a-77a8eac79483", + Recipient: "jordan@example.com", + DeliveredAt: "2026-07-14T16:33:54.907Z", + Details: + "smtp;250 2.0.0 OK 1752510834 a1-20020a17090a5a0100b002b0c1b1c1a1si1234567pjd.1 - gsmtp", + Tag: "welcome-email", + ServerID: 23, + Metadata: { + order_id: "ord_4f9c2b1a", + }, + }, + docsUrl: "https://postmarkapp.com/developer/webhooks/delivery-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "postmark", + providerLabel: "Postmark", + eventType: "Bounce", + name: "Hard bounce", + description: "The recipient's mail server permanently rejected the message (unknown user).", + body: { + RecordType: "Bounce", + MessageStream: "outbound", + ID: 42323002, + Type: "HardBounce", + TypeCode: 1, + Name: "Hard bounce", + Tag: "welcome-email", + MessageID: "883953f4-6105-42a2-a16a-77a8eac79483", + Metadata: { + order_id: "ord_4f9c2b1a", + }, + ServerID: 23, + Description: + "The server was unable to deliver your message (ex: unknown user, mailbox not found).", + Details: "smtp;550 5.1.1 The email account that you tried to reach does not exist.", + Email: "morgan@example.com", + From: "notifications@acme.dev", + BouncedAt: "2026-07-14T16:41:02.113Z", + DumpAvailable: true, + Inactive: true, + CanActivate: true, + Subject: "Welcome to Acme", + }, + docsUrl: "https://postmarkapp.com/developer/webhooks/bounce-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "postmark", + providerLabel: "Postmark", + eventType: "SpamComplaint", + name: "Spam complaint", + description: "A recipient marked the message as spam via their mail provider's feedback loop.", + body: { + RecordType: "SpamComplaint", + MessageStream: "outbound", + ID: 8675309, + Type: "SpamComplaint", + TypeCode: 512, + Name: "Spam complaint", + Tag: "newsletter", + MessageID: "7c3e9a52-6f18-4d2b-b9a7-1e5c8f3d2a64", + Metadata: { + campaign_id: "camp_2026_07", + }, + ServerID: 23, + Description: "", + Details: "Feedback loop report from recipient's mailbox provider.", + Email: "sam@example.com", + From: "news@acme.dev", + BouncedAt: "2026-07-14T17:02:19.442Z", + DumpAvailable: true, + Inactive: true, + CanActivate: false, + Subject: "Your July product update", + }, + docsUrl: "https://postmarkapp.com/developer/webhooks/spam-complaint-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "postmark", + providerLabel: "Postmark", + eventType: "Open", + name: "Email opened", + description: "The recipient's mail client rendered the tracking pixel for a sent message.", + body: { + RecordType: "Open", + MessageStream: "outbound", + FirstOpen: true, + Client: { + Name: "Chrome 126.0.0.0", + Company: "Google", + Family: "Chrome", + }, + OS: { + Name: "macOS 14", + Company: "Apple Computer, Inc.", + Family: "macOS", + }, + Platform: "WebMail", + UserAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", + Geo: { + CountryISOCode: "US", + Country: "United States", + RegionISOCode: "CA", + Region: "California", + City: "San Francisco", + Zip: "94103", + Coords: "37.7749,-122.4194", + IP: "203.0.113.42", + }, + MessageID: "883953f4-6105-42a2-a16a-77a8eac79483", + Metadata: { + order_id: "ord_4f9c2b1a", + }, + ReceivedAt: "2026-07-14T16:35:10.221Z", + Tag: "welcome-email", + Recipient: "jordan@example.com", + }, + docsUrl: "https://postmarkapp.com/developer/webhooks/open-tracking-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "postmark", + providerLabel: "Postmark", + eventType: "Inbound", + name: "Inbound email received", + description: + "An email sent to a Postmark inbound address was parsed and posted as JSON. Unlike the event webhooks above, this payload has no `RecordType` field.", + body: { + FromName: "Priya Nair", + MessageStream: "inbound", + From: "priya@example.com", + FromFull: { + Email: "priya@example.com", + Name: "Priya Nair", + MailboxHash: "", + }, + To: '"Acme Support" ', + ToFull: [ + { + Email: "a1b2c3d4e5f6@inbound.postmarkapp.com", + Name: "Acme Support", + MailboxHash: "", + }, + ], + Cc: "", + CcFull: [], + Bcc: "", + BccFull: [], + OriginalRecipient: "a1b2c3d4e5f6@inbound.postmarkapp.com", + Subject: "Trouble connecting my webhook endpoint", + MessageID: "3b2d1a9e-2f4c-4e51-9a7d-6c8f2e1b4a90", + ReplyTo: "priya@example.com", + MailboxHash: "", + Date: "Tue, 14 Jul 2026 11:20:00 -0400", + TextBody: "Hi, our webhook endpoint keeps returning a 401. Can you check the delivery logs?", + HtmlBody: + "

Hi, our webhook endpoint keeps returning a 401. Can you check the delivery logs?

", + StrippedTextReply: "", + Tag: "", + Headers: [ + { Name: "X-Spam-Status", Value: "No" }, + { Name: "X-Spam-Score", Value: "-0.3" }, + ], + Attachments: [], + }, + docsUrl: "https://postmarkapp.com/developer/webhooks/inbound-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/recall-ai.ts b/internal-packages/webhook-sources/src/handAuthored/recall-ai.ts new file mode 100644 index 00000000000..92d3d2d8855 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/recall-ai.ts @@ -0,0 +1,141 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Recall.ai samples. Bot and recording/transcript artifact webhooks share one envelope shape + * `{ event, data: { data: { code, sub_code, updated_at }, ...refs } }`. Recall.ai signs with Svix, so + * `presetId: "svix"` keeps them under the round-trip guarantee. + */ +export const samples: SampleRecord[] = [ + { + provider: "recall-ai", + providerLabel: "Recall.ai", + presetId: "svix", + eventType: "bot.status_change", + name: "Bot recording", + description: "A meeting bot has joined the call and started recording.", + body: { + event: "bot.status_change", + data: { + data: { + code: "in_call_recording", + sub_code: null, + updated_at: "2026-07-14T18:22:03.104Z", + }, + bot: { + id: "6f2b1a3e-9c4d-4b7a-8e2f-1d5c9a7b3e21", + metadata: {}, + }, + }, + }, + docsUrl: "https://docs.recall.ai/docs/bot-status-change-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "recall-ai", + providerLabel: "Recall.ai", + presetId: "svix", + eventType: "bot.status_change", + name: "Bot call ended by host", + description: "The meeting host ended the call, so the bot's call has ended.", + body: { + event: "bot.status_change", + data: { + data: { + code: "call_ended", + sub_code: "call_ended_by_host", + updated_at: "2026-07-14T18:47:11.882Z", + }, + bot: { + id: "6f2b1a3e-9c4d-4b7a-8e2f-1d5c9a7b3e21", + metadata: {}, + }, + }, + }, + docsUrl: "https://docs.recall.ai/docs/bot-status-change-events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "recall-ai", + providerLabel: "Recall.ai", + presetId: "svix", + eventType: "bot.status_change", + name: "Bot fatal error", + description: "The bot ran into an unexpected, unrecoverable error before it could join.", + body: { + event: "bot.status_change", + data: { + data: { + code: "fatal", + sub_code: "bot_errored", + updated_at: "2026-07-14T18:15:47.221Z", + }, + bot: { + id: "a184e6d7-2f3b-4c9e-8a11-6b2d9f4e7c30", + metadata: {}, + }, + }, + }, + docsUrl: "https://docs.recall.ai/docs/sub-codes", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "recall-ai", + providerLabel: "Recall.ai", + presetId: "svix", + eventType: "recording.done", + name: "Recording done", + description: "A bot's recording finished processing and is ready to fetch.", + body: { + event: "recording.done", + data: { + data: { + code: "done", + sub_code: null, + updated_at: "2026-07-14T18:47:22.556Z", + }, + recording: { + id: "3d9e6a2f-7b4c-4e1a-9f8d-2c5b8e1a4d67", + metadata: {}, + }, + bot: { + id: "6f2b1a3e-9c4d-4b7a-8e2f-1d5c9a7b3e21", + metadata: {}, + }, + }, + }, + docsUrl: "https://docs.recall.ai/docs/recording-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "recall-ai", + providerLabel: "Recall.ai", + presetId: "svix", + eventType: "transcript.done", + name: "Transcript done", + description: "A bot's transcript finished processing and is ready to fetch.", + body: { + event: "transcript.done", + data: { + data: { + code: "done", + sub_code: null, + updated_at: "2026-07-14T18:47:45.913Z", + }, + transcript: { + id: "9b3f5e7c-1a2d-4c8e-b6f9-3e7d1c9a5b42", + metadata: {}, + }, + recording: { + id: "3d9e6a2f-7b4c-4e1a-9f8d-2c5b8e1a4d67", + metadata: {}, + }, + bot: { + id: "6f2b1a3e-9c4d-4b7a-8e2f-1d5c9a7b3e21", + metadata: {}, + }, + }, + }, + docsUrl: "https://docs.recall.ai/docs/bot-async-transcription", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/replicate.ts b/internal-packages/webhook-sources/src/handAuthored/replicate.ts new file mode 100644 index 00000000000..2918d87ec6d --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/replicate.ts @@ -0,0 +1,191 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Replicate samples. Replicate signs webhooks with a Standard Webhooks compatible scheme (`webhook-id` + * / `webhook-timestamp` / `webhook-signature` headers, HMAC-SHA256 over `{id}.{timestamp}.{rawBody}`, + * `whsec_`-prefixed secret), so `presetId: "svix"` keeps them under the round-trip guarantee. The body + * is the prediction object itself; there is no separate event envelope, so `eventType` is read from the + * prediction's own `status` field. + */ +export const samples: SampleRecord[] = [ + { + provider: "replicate", + providerLabel: "Replicate", + presetId: "svix", + eventType: "starting", + name: "Prediction starting", + description: "A prediction has been created and queued but has not started running yet.", + body: { + id: "qz7mfxlkw3vdpygt1o8hbnsuae", + model: "black-forest-labs/flux-schnell", + version: "a3f1c9d2b7e4809f61a2d5c8e0b3f7a41d6c9b2e5f80a3d17c4b9e2f6a0d3c85", + input: { + prompt: "a studio photo of a golden retriever wearing sunglasses", + num_outputs: 1, + aspect_ratio: "1:1", + output_format: "webp", + }, + logs: null, + output: null, + error: null, + status: "starting", + created_at: "2026-07-14T09:12:03.221000Z", + source: "api", + data_removed: false, + started_at: null, + completed_at: null, + metrics: {}, + urls: { + web: "https://replicate.com/p/qz7mfxlkw3vdpygt1o8hbnsuae", + get: "https://api.replicate.com/v1/predictions/qz7mfxlkw3vdpygt1o8hbnsuae", + cancel: "https://api.replicate.com/v1/predictions/qz7mfxlkw3vdpygt1o8hbnsuae/cancel", + }, + }, + docsUrl: "https://replicate.com/docs/topics/webhooks/receive-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "replicate", + providerLabel: "Replicate", + presetId: "svix", + eventType: "processing", + name: "Prediction processing", + description: "The model has started running; intermediate logs are streaming in.", + body: { + id: "n5tqorvfy28mzbklwdshxc1gpu", + model: "black-forest-labs/flux-schnell", + version: "a3f1c9d2b7e4809f61a2d5c8e0b3f7a41d6c9b2e5f80a3d17c4b9e2f6a0d3c85", + input: { + prompt: "a watercolor painting of a lighthouse at sunset", + num_outputs: 1, + aspect_ratio: "16:9", + output_format: "webp", + }, + logs: "Using seed: 48219\nGenerating image...\n", + output: null, + error: null, + status: "processing", + created_at: "2026-07-14T09:13:41.009000Z", + source: "api", + data_removed: false, + started_at: "2026-07-14T09:13:42.552000Z", + completed_at: null, + metrics: {}, + urls: { + web: "https://replicate.com/p/n5tqorvfy28mzbklwdshxc1gpu", + get: "https://api.replicate.com/v1/predictions/n5tqorvfy28mzbklwdshxc1gpu", + cancel: "https://api.replicate.com/v1/predictions/n5tqorvfy28mzbklwdshxc1gpu/cancel", + }, + }, + docsUrl: "https://replicate.com/docs/topics/webhooks/receive-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "replicate", + providerLabel: "Replicate", + presetId: "svix", + eventType: "succeeded", + name: "Prediction succeeded", + description: "The prediction finished and produced output.", + body: { + id: "x9blkfqm37ydrpwnztgohsuic", + model: "black-forest-labs/flux-schnell", + version: "a3f1c9d2b7e4809f61a2d5c8e0b3f7a41d6c9b2e5f80a3d17c4b9e2f6a0d3c85", + input: { + prompt: "a studio photo of a golden retriever wearing sunglasses", + num_outputs: 1, + aspect_ratio: "1:1", + output_format: "webp", + }, + logs: "Using seed: 17734\nGenerating image...\nDone in 0.94s\n", + output: ["https://replicate.delivery/pbxt/QuAty1fakeExampleHash000/out-0.webp"], + error: null, + status: "succeeded", + created_at: "2026-07-14T09:14:55.114000Z", + source: "api", + data_removed: false, + started_at: "2026-07-14T09:14:55.930000Z", + completed_at: "2026-07-14T09:14:56.871000Z", + metrics: { predict_time: 0.941263 }, + urls: { + web: "https://replicate.com/p/x9blkfqm37ydrpwnztgohsuic", + get: "https://api.replicate.com/v1/predictions/x9blkfqm37ydrpwnztgohsuic", + cancel: "https://api.replicate.com/v1/predictions/x9blkfqm37ydrpwnztgohsuic/cancel", + }, + }, + docsUrl: "https://replicate.com/docs/topics/webhooks/receive-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "replicate", + providerLabel: "Replicate", + presetId: "svix", + eventType: "failed", + name: "Prediction failed", + description: "The model raised an error while running and the prediction did not complete.", + body: { + id: "f3wgzqmnyt8blkdrpxhcvos19u", + model: "black-forest-labs/flux-schnell", + version: "a3f1c9d2b7e4809f61a2d5c8e0b3f7a41d6c9b2e5f80a3d17c4b9e2f6a0d3c85", + input: { + prompt: "an ultra high resolution 8k panorama of a mountain range", + num_outputs: 4, + aspect_ratio: "21:9", + output_format: "webp", + }, + logs: "Using seed: 90271\nGenerating image...\n", + output: null, + error: "CUDA out of memory. Tried to allocate 2.00 GiB (GPU 0; 39.39 GiB total capacity)", + status: "failed", + created_at: "2026-07-14T09:16:02.447000Z", + source: "api", + data_removed: false, + started_at: "2026-07-14T09:16:03.018000Z", + completed_at: "2026-07-14T09:16:07.335000Z", + metrics: { predict_time: 4.317092 }, + urls: { + web: "https://replicate.com/p/f3wgzqmnyt8blkdrpxhcvos19u", + get: "https://api.replicate.com/v1/predictions/f3wgzqmnyt8blkdrpxhcvos19u", + cancel: "https://api.replicate.com/v1/predictions/f3wgzqmnyt8blkdrpxhcvos19u/cancel", + }, + }, + docsUrl: "https://replicate.com/docs/topics/webhooks/receive-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "replicate", + providerLabel: "Replicate", + presetId: "svix", + eventType: "canceled", + name: "Prediction canceled", + description: "The prediction was canceled via the cancel endpoint before it finished running.", + body: { + id: "h7ldyxqmnzt3fgbkwrpc850osu", + model: "black-forest-labs/flux-schnell", + version: "a3f1c9d2b7e4809f61a2d5c8e0b3f7a41d6c9b2e5f80a3d17c4b9e2f6a0d3c85", + input: { + prompt: "a neon-lit cyberpunk street market at night", + num_outputs: 1, + aspect_ratio: "3:2", + output_format: "webp", + }, + logs: "Using seed: 55810\nGenerating image...\n", + output: null, + error: null, + status: "canceled", + created_at: "2026-07-14T09:17:20.663000Z", + source: "api", + data_removed: false, + started_at: "2026-07-14T09:17:21.204000Z", + completed_at: "2026-07-14T09:17:23.489000Z", + metrics: {}, + urls: { + web: "https://replicate.com/p/h7ldyxqmnzt3fgbkwrpc850osu", + get: "https://api.replicate.com/v1/predictions/h7ldyxqmnzt3fgbkwrpc850osu", + cancel: "https://api.replicate.com/v1/predictions/h7ldyxqmnzt3fgbkwrpc850osu/cancel", + }, + }, + docsUrl: "https://replicate.com/docs/topics/webhooks/receive-webhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/resend.ts b/internal-packages/webhook-sources/src/handAuthored/resend.ts new file mode 100644 index 00000000000..39cc66e7899 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/resend.ts @@ -0,0 +1,165 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Resend samples. Resend signs webhooks with Svix (`svix-id`/`svix-timestamp`/`svix-signature` + * headers, HMAC-SHA256 base64 over `{id}.{timestamp}.{rawBody}`, `whsec_`-prefixed secret), so + * `presetId: "svix"` keeps them under the round-trip guarantee. `broadcast_id`/`template_id` only + * appear on `data` when the email was sent as part of a Broadcast or with a Template. + */ +export const samples: SampleRecord[] = [ + { + provider: "resend", + providerLabel: "Resend", + presetId: "svix", + eventType: "email.sent", + name: "Email sent", + description: + "Resend accepted the API request and is attempting delivery to the recipient's mail server.", + body: { + type: "email.sent", + created_at: "2026-06-18T09:12:03.041Z", + data: { + created_at: "2026-06-18T09:12:02.884Z", + email_id: "4ef9a417-02e9-4d39-8e9d-3f1c6a2b9c11", + message_id: "", + from: "Acme ", + to: ["jamie@example.com"], + subject: "Welcome to Acme", + tags: { category: "welcome_email" }, + }, + }, + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "resend", + providerLabel: "Resend", + presetId: "svix", + eventType: "email.delivered", + name: "Email delivered", + description: "The recipient's mail server accepted the email.", + body: { + type: "email.delivered", + created_at: "2026-06-18T09:12:41.317Z", + data: { + broadcast_id: "8f2c6d7a-1b3e-4a9c-9d5f-6e2a8b4c7f10", + created_at: "2026-06-18T09:12:40.955Z", + email_id: "7c3e9a52-6f18-4d2b-b9a7-1e5c8f3d2a64", + message_id: "", + from: "Acme ", + to: ["morgan@example.com"], + subject: "Your July product update", + template_id: "d4e5f6a7-8b9c-4d1e-af23-456789012bcd", + tags: { category: "newsletter" }, + }, + }, + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "resend", + providerLabel: "Resend", + presetId: "svix", + eventType: "email.bounced", + name: "Email bounced", + description: "The recipient's mail server permanently rejected the email.", + body: { + type: "email.bounced", + created_at: "2026-06-18T09:13:05.662Z", + data: { + created_at: "2026-06-18T09:13:05.201Z", + email_id: "9b1d4f7a-2c5e-4b8d-a1f3-6c9e2b5d8a17", + message_id: "", + from: "Acme ", + to: ["taylor@example.com"], + subject: "Confirm your email address", + bounce: { + type: "Permanent", + subType: "Suppressed", + message: + "The recipient's email address is on the suppression list because it has a recent history of producing hard bounces.", + }, + tags: { category: "confirm_email" }, + }, + }, + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "resend", + providerLabel: "Resend", + presetId: "svix", + eventType: "email.complained", + name: "Email complained", + description: "The email was delivered, but the recipient marked it as spam.", + body: { + type: "email.complained", + created_at: "2026-06-18T09:14:22.108Z", + data: { + created_at: "2026-06-18T09:14:21.774Z", + email_id: "2a5c8e1b-4d7f-4a3c-9b6e-8f1c4a7d2e59", + message_id: "", + from: "Acme ", + to: ["riley@example.com"], + subject: "Flash sale ends tonight", + tags: { category: "promo" }, + }, + }, + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "resend", + providerLabel: "Resend", + presetId: "svix", + eventType: "email.opened", + name: "Email opened", + description: "The recipient opened the email.", + body: { + type: "email.opened", + created_at: "2026-06-18T09:20:11.933Z", + data: { + created_at: "2026-06-18T09:12:02.884Z", + email_id: "4ef9a417-02e9-4d39-8e9d-3f1c6a2b9c11", + message_id: "", + from: "Acme ", + to: ["jamie@example.com"], + subject: "Welcome to Acme", + tags: { category: "welcome_email" }, + }, + }, + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "resend", + providerLabel: "Resend", + presetId: "svix", + eventType: "email.clicked", + name: "Email link clicked", + description: "The recipient clicked a link inside the email.", + body: { + type: "email.clicked", + created_at: "2026-06-18T09:25:47.502Z", + data: { + created_at: "2026-06-18T09:12:40.955Z", + email_id: "7c3e9a52-6f18-4d2b-b9a7-1e5c8f3d2a64", + message_id: "", + from: "Acme ", + to: ["morgan@example.com"], + subject: "Your July product update", + template_id: "d4e5f6a7-8b9c-4d1e-af23-456789012bcd", + click: { + link: "https://acme.dev/pricing", + ipAddress: "203.0.113.42", + timestamp: "2026-06-18T09:25:47.502Z", + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15", + }, + tags: { category: "newsletter" }, + }, + }, + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/retell.ts b/internal-packages/webhook-sources/src/handAuthored/retell.ts new file mode 100644 index 00000000000..0d0bc48574f --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/retell.ts @@ -0,0 +1,192 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Retell AI samples. Retell signs with `x-retell-signature: v={timestamp_ms},d={hex_digest}` + * (HMAC-SHA256 over `rawBody + timestamp`), which does not match any of our verifier presets, so + * these ship sample-only (no `presetId`). Every event is a thin envelope (`event`, `call`) where + * `call` grows richer across the lifecycle: `call_started` carries basic call info, `call_ended` + * adds timing/transcript/disconnection details, and `call_analyzed` adds the `call_analysis` object. + */ +export const samples: SampleRecord[] = [ + { + provider: "retell", + providerLabel: "Retell AI", + eventType: "call_started", + name: "Call started (phone)", + description: + "An inbound phone call was answered by the agent. `call.call_status` is `ongoing`.", + body: { + event: "call_started", + call: { + call_type: "phone_call", + from_number: "+12137771234", + to_number: "+12137771235", + direction: "inbound", + call_id: "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6", + agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", + call_status: "ongoing", + metadata: {}, + retell_llm_dynamic_variables: { + customer_name: "John Doe", + }, + start_timestamp: 1752562800000, + opt_out_sensitive_data_storage: false, + }, + }, + docsUrl: "https://docs.retellai.com/features/webhook-overview", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "retell", + providerLabel: "Retell AI", + eventType: "call_started", + name: "Call started (web)", + description: + "A browser-based web call began. `call.call_type` is `web_call` and there are no phone numbers.", + body: { + event: "call_started", + call: { + call_type: "web_call", + call_id: "kNc4mQZLPQfrTv8XoypA55rdYHmqTXcb", + agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", + call_status: "ongoing", + metadata: { + session_source: "docs-widget", + }, + retell_llm_dynamic_variables: { + customer_name: "Priya Nair", + }, + start_timestamp: 1752563400000, + opt_out_sensitive_data_storage: false, + }, + }, + docsUrl: "https://docs.retellai.com/features/webhook-overview", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "retell", + providerLabel: "Retell AI", + eventType: "call_ended", + name: "Call ended (user hangup)", + description: + "The call finished normally. `call.disconnection_reason` is `user_hangup`; `call.transcript` and `call.transcript_object` hold the full conversation.", + body: { + event: "call_ended", + call: { + call_type: "phone_call", + from_number: "+12137771234", + to_number: "+12137771235", + direction: "inbound", + call_id: "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6", + agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", + call_status: "ended", + metadata: {}, + retell_llm_dynamic_variables: { + customer_name: "John Doe", + }, + start_timestamp: 1752562800000, + end_timestamp: 1752562875000, + disconnection_reason: "user_hangup", + transcript: + "Agent: Thanks for calling Acme Support, how can I help?\nUser: I need to reschedule my delivery.\nAgent: Sure, what date works best for you?\nUser: Next Tuesday please.", + transcript_object: [ + { role: "agent", content: "Thanks for calling Acme Support, how can I help?", words: [] }, + { role: "user", content: "I need to reschedule my delivery.", words: [] }, + { role: "agent", content: "Sure, what date works best for you?", words: [] }, + { role: "user", content: "Next Tuesday please.", words: [] }, + ], + recording_url: "https://retell-recordings.example.com/Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6.wav", + opt_out_sensitive_data_storage: false, + }, + }, + docsUrl: "https://docs.retellai.com/features/webhook-overview", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "retell", + providerLabel: "Retell AI", + eventType: "call_ended", + name: "Call ended (voicemail)", + description: + "The call reached voicemail and Retell hung up. `call.disconnection_reason` is `voicemail_reached`.", + body: { + event: "call_ended", + call: { + call_type: "phone_call", + from_number: "+14155550111", + to_number: "+14155550199", + direction: "outbound", + call_id: "wRq2vBXNPLmztK9GhopY73rdCHmvTScf", + agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", + call_status: "ended", + metadata: { + campaign_id: "appt-reminder-2026-07", + }, + retell_llm_dynamic_variables: { + customer_name: "Mateo Alvarez", + }, + start_timestamp: 1752564000000, + end_timestamp: 1752564018000, + disconnection_reason: "voicemail_reached", + transcript: "Agent: Hi, this is a reminder about your appointment tomorrow at 10am.", + transcript_object: [ + { + role: "agent", + content: "Hi, this is a reminder about your appointment tomorrow at 10am.", + words: [], + }, + ], + recording_url: "https://retell-recordings.example.com/wRq2vBXNPLmztK9GhopY73rdCHmvTScf.wav", + opt_out_sensitive_data_storage: false, + }, + }, + docsUrl: "https://docs.retellai.com/features/webhook-overview", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "retell", + providerLabel: "Retell AI", + eventType: "call_analyzed", + name: "Call analyzed", + description: + "Post-call analysis finished. `call.call_analysis` carries the summary, sentiment, and success flag.", + body: { + event: "call_analyzed", + call: { + call_type: "phone_call", + from_number: "+12137771234", + to_number: "+12137771235", + direction: "inbound", + call_id: "Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6", + agent_id: "oBeDLoLOeuAbiuaMFXRtDOLriTJ5tSxD", + call_status: "ended", + metadata: {}, + retell_llm_dynamic_variables: { + customer_name: "John Doe", + }, + start_timestamp: 1752562800000, + end_timestamp: 1752562875000, + duration_ms: 75000, + disconnection_reason: "user_hangup", + transcript: + "Agent: Thanks for calling Acme Support, how can I help?\nUser: I need to reschedule my delivery.\nAgent: Sure, what date works best for you?\nUser: Next Tuesday please.", + recording_url: "https://retell-recordings.example.com/Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6.wav", + public_log_url: "https://retell-logs.example.com/Jabr9TXYYJHfvl6Syypi88rdAHYHmcq6/log.json", + call_analysis: { + call_summary: + "Customer called to reschedule a delivery to next Tuesday; agent confirmed the new date.", + user_sentiment: "Positive", + call_successful: true, + in_voicemail: false, + custom_analysis_data: { + reschedule_requested: true, + new_delivery_date: "2026-07-21", + }, + }, + opt_out_sensitive_data_storage: false, + }, + }, + docsUrl: "https://docs.retellai.com/features/webhook-overview", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/sendgrid.ts b/internal-packages/webhook-sources/src/handAuthored/sendgrid.ts new file mode 100644 index 00000000000..9152511a1b5 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/sendgrid.ts @@ -0,0 +1,125 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * SendGrid Event Webhook samples. SendGrid signs with ECDSA (`X-Twilio-Email-Event-Webhook-Signature` + * + `-Timestamp` headers, verified with an EC public key SendGrid issues), which does not match any of + * our verifier presets, so these ship sample-only (no `presetId`). SendGrid always POSTs a JSON ARRAY + * of event objects in one batch; each sample here is a single-element array holding one representative + * event, and `eventType` is that event's `event` value. + */ +export const samples: SampleRecord[] = [ + { + provider: "sendgrid", + providerLabel: "SendGrid", + eventType: "delivered", + name: "Email delivered", + description: "SendGrid accepted and delivered the message to the receiving server.", + body: [ + { + email: "jordan@example.com", + timestamp: 1752562800, + "smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>", + event: "delivered", + category: ["welcome-series"], + sg_event_id: "rWVYmVk90MjZJ9iohOBa3w_fake==", + sg_message_id: "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0", + response: "250 OK", + asm_group_id: 1, + }, + ], + docsUrl: "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sendgrid", + providerLabel: "SendGrid", + eventType: "open", + name: "Email opened", + description: + "The recipient opened the message. `sg_machine_open` flags automated/prefetch opens.", + body: [ + { + email: "jordan@example.com", + timestamp: 1752563400, + event: "open", + sg_machine_open: false, + category: ["welcome-series"], + sg_event_id: "FOTFFO0ecsBE-zxFXfs6WA_fake==", + sg_message_id: "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0", + useragent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15", + ip: "198.51.100.23", + }, + ], + docsUrl: "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sendgrid", + providerLabel: "SendGrid", + eventType: "click", + name: "Link clicked", + description: "The recipient clicked a tracked link in the message.", + body: [ + { + email: "jordan@example.com", + timestamp: 1752564000, + event: "click", + category: ["welcome-series"], + sg_event_id: "sg-click-event-id-fake-0001", + sg_message_id: "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0", + useragent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)", + ip: "198.51.100.23", + url: "https://example.com/welcome", + url_offset: { index: 0, type: "html" }, + }, + ], + docsUrl: "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sendgrid", + providerLabel: "SendGrid", + eventType: "bounce", + name: "Email bounced", + description: + "The receiving server permanently rejected the message. `reason` and `status` carry the SMTP diagnostic.", + body: [ + { + email: "unknown@example.com", + timestamp: 1752564600, + "smtp-id": "<14c5d75ce93.dfd.64b470@ismtpd-555>", + bounce_classification: "Invalid Address", + event: "bounce", + category: ["welcome-series"], + sg_event_id: "6g4ZI7SA-xmRDv57GoPIPw_fake==", + sg_message_id: "14c5d75ce93.dfd.64b470.filter0001.16648.5515E0B88.0", + reason: "550 5.1.1 The email account that you tried to reach does not exist", + status: "5.1.1", + type: "bounce", + tls: true, + }, + ], + docsUrl: "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sendgrid", + providerLabel: "SendGrid", + eventType: "spamreport", + name: "Marked as spam", + description: "The recipient reported the message as spam via their mail client.", + body: [ + { + email: "jordan@example.com", + timestamp: 1752565200, + "smtp-id": "<14c5d75ce93.dfd.64b471@ismtpd-555>", + event: "spamreport", + category: ["welcome-series"], + sg_event_id: "37nvH5QBz858KGVYCM4uOA_fake==", + sg_message_id: "14c5d75ce93.dfd.64b471.filter0001.16648.5515E0B88.0", + }, + ], + docsUrl: "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/sentry.ts b/internal-packages/webhook-sources/src/handAuthored/sentry.ts new file mode 100644 index 00000000000..e813be66661 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/sentry.ts @@ -0,0 +1,260 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Sentry Integration Platform webhook samples. The resource type (issue/error) travels in the + * `Sentry-Hook-Resource` header, not the body, so `eventType` here is the synthetic + * `.` string and each sample carries a matching `extraHeaders["sentry-hook-resource"]` + * so the type survives loading. No preset applies (Sentry's signature scheme is bespoke), so no + * `presetId` is set. + */ +export const samples: SampleRecord[] = [ + { + provider: "sentry", + providerLabel: "Sentry", + eventType: "issue.created", + name: "Issue created", + description: "A new issue was created from an ingested error event.", + body: { + action: "created", + installation: { + uuid: "24b397fc-a86e-43ef-9297-949e21b82480", + }, + data: { + issue: { + id: "4509877862268928", + url: "https://sentry.io/api/0/organizations/acme-co/issues/4509877862268928/", + web_url: "https://acme-co.sentry.io/issues/4509877862268928/", + project_url: "https://acme-co.sentry.io/issues/?project=4509866123456789", + shortId: "NODE-API-7", + title: "TypeError: Cannot read properties of undefined (reading 'id')", + culprit: "processOrder(app/services/orders.ts)", + permalink: "https://acme-co.sentry.io/issues/4509877862268928/", + logger: null, + level: "error", + status: "unresolved", + substatus: "new", + statusDetails: {}, + issueType: "error", + issueCategory: "error", + priority: "high", + count: "1", + userCount: 1, + firstSeen: "2026-07-14T18:56:00.679000+00:00", + lastSeen: "2026-07-14T18:56:00.738000+00:00", + project: { + id: "4509866123456789", + name: "node-api", + slug: "node-api", + platform: "node", + }, + }, + }, + actor: { + type: "application", + id: "sentry", + name: "Sentry", + }, + }, + extraHeaders: { + "sentry-hook-resource": "issue.created", + }, + docsUrl: "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sentry", + providerLabel: "Sentry", + eventType: "issue.resolved", + name: "Issue resolved", + description: "A team member resolved an issue from the Sentry UI.", + body: { + action: "resolved", + installation: { + uuid: "24b397fc-a86e-43ef-9297-949e21b82480", + }, + data: { + issue: { + id: "4509877862268928", + url: "https://sentry.io/api/0/organizations/acme-co/issues/4509877862268928/", + web_url: "https://acme-co.sentry.io/issues/4509877862268928/", + project_url: "https://acme-co.sentry.io/issues/?project=4509866123456789", + shortId: "NODE-API-7", + title: "TypeError: Cannot read properties of undefined (reading 'id')", + culprit: "processOrder(app/services/orders.ts)", + permalink: "https://acme-co.sentry.io/issues/4509877862268928/", + logger: null, + level: "error", + status: "resolved", + substatus: null, + statusDetails: { + inNextRelease: true, + }, + issueType: "error", + issueCategory: "error", + priority: "high", + count: "14", + userCount: 6, + firstSeen: "2026-07-12T09:12:44.201000+00:00", + lastSeen: "2026-07-14T18:56:00.738000+00:00", + project: { + id: "4509866123456789", + name: "node-api", + slug: "node-api", + platform: "node", + }, + }, + }, + actor: { + type: "user", + id: "56789", + name: "Morgan Reyes", + email: "morgan@example.com", + }, + }, + extraHeaders: { + "sentry-hook-resource": "issue.resolved", + }, + docsUrl: "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sentry", + providerLabel: "Sentry", + eventType: "issue.assigned", + name: "Issue assigned", + description: "An issue was assigned to a team member.", + body: { + action: "assigned", + installation: { + uuid: "24b397fc-a86e-43ef-9297-949e21b82480", + }, + data: { + issue: { + id: "4509877862299001", + url: "https://sentry.io/api/0/organizations/acme-co/issues/4509877862299001/", + web_url: "https://acme-co.sentry.io/issues/4509877862299001/", + project_url: "https://acme-co.sentry.io/issues/?project=4509866123456789", + shortId: "NODE-API-11", + title: "Timeout connecting to redis://cache-primary:6379", + culprit: "RedisClient.connect(app/lib/cache.ts)", + permalink: "https://acme-co.sentry.io/issues/4509877862299001/", + logger: null, + level: "warning", + status: "unresolved", + substatus: "ongoing", + statusDetails: {}, + issueType: "error", + issueCategory: "error", + priority: "medium", + count: "42", + userCount: 9, + firstSeen: "2026-07-10T03:41:19.442000+00:00", + lastSeen: "2026-07-14T17:03:52.114000+00:00", + project: { + id: "4509866123456789", + name: "node-api", + slug: "node-api", + platform: "node", + }, + }, + assignee: { + type: "user", + id: "56789", + name: "Morgan Reyes", + email: "morgan@example.com", + }, + }, + actor: { + type: "user", + id: "56789", + name: "Morgan Reyes", + email: "morgan@example.com", + }, + }, + extraHeaders: { + "sentry-hook-resource": "issue.assigned", + }, + docsUrl: "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "sentry", + providerLabel: "Sentry", + eventType: "error.created", + name: "Error event created", + description: "A new error event was ingested and matched to an issue.", + body: { + action: "created", + installation: { + uuid: "24b397fc-a86e-43ef-9297-949e21b82480", + }, + data: { + error: { + event_id: "f2f4b3c8e1a94d6f9b3d2c7a5e8f1b40", + issue_id: "4509877862268928", + url: "https://sentry.io/api/0/projects/acme-co/node-api/events/f2f4b3c8e1a94d6f9b3d2c7a5e8f1b40/", + web_url: + "https://acme-co.sentry.io/issues/4509877862268928/events/f2f4b3c8e1a94d6f9b3d2c7a5e8f1b40/", + issue_url: "https://sentry.io/api/0/organizations/acme-co/issues/4509877862268928/", + project: "node-api", + type: "error", + title: "TypeError: Cannot read properties of undefined (reading 'id')", + message: "Cannot read properties of undefined (reading 'id')", + level: "error", + culprit: "processOrder(app/services/orders.ts)", + platform: "node", + sdk: { + name: "sentry.javascript.node", + version: "8.42.0", + }, + exception: { + values: [ + { + type: "TypeError", + value: "Cannot read properties of undefined (reading 'id')", + mechanism: { type: "onunhandledrejection", handled: false }, + stacktrace: { + frames: [ + { + filename: "app/services/orders.ts", + function: "processOrder", + lineno: 42, + colno: 18, + in_app: true, + }, + ], + }, + }, + ], + }, + contexts: { + os: { name: "Linux", version: "6.2.0" }, + runtime: { name: "node", version: "20.14.0" }, + }, + tags: [ + ["environment", "production"], + ["level", "error"], + ["release", "node-api@2026.7.3"], + ], + user: { + id: "38214", + email: "customer@example.com", + }, + datetime: "2026-07-14T18:56:00.679000Z", + timestamp: 1786820160.679, + received: 1786820160.812, + }, + }, + actor: { + type: "application", + id: "sentry", + name: "Sentry", + }, + }, + extraHeaders: { + "sentry-hook-resource": "error.created", + }, + docsUrl: "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/slack.ts b/internal-packages/webhook-sources/src/handAuthored/slack.ts new file mode 100644 index 00000000000..0ed0185a589 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/slack.ts @@ -0,0 +1,196 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Slack Events API samples. Slack wraps every event in an `event_callback` envelope and nests the + * actual event under `event`, so `eventType` here mirrors `event.type`, not the outer `type`. Note + * that channel-scoped subscriptions like `message.channels` still carry `event.type === "message"` + * on the wire, so there is no separate "message.channels" sample. + */ +export const samples: SampleRecord[] = [ + { + provider: "slack", + providerLabel: "Slack", + eventType: "app_mention", + name: "App mentioned", + description: "The bot was @-mentioned in a channel.", + body: { + token: "Jhj5dZrVaK7ZwHHjRyZWjbDl", + team_id: "T024BE7LD", + api_app_id: "A0MDYCDME", + event: { + type: "app_mention", + user: "U061F7AUR", + text: "<@U0LAN0Z89> can you deploy the app?", + ts: "1515449522.000016", + channel: "C0T2GACDT", + event_ts: "1515449522000016", + }, + type: "event_callback", + authorizations: [ + { + team_id: "T024BE7LD", + user_id: "U0LAN0Z89", + is_bot: true, + is_enterprise_install: false, + }, + ], + event_id: "Ev0MDYA7HL", + event_time: 1515449522, + }, + docsUrl: "https://docs.slack.dev/reference/events/app_mention/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "slack", + providerLabel: "Slack", + eventType: "message", + name: "Message posted in a channel", + description: "A plain message was posted to a public channel the bot is a member of.", + body: { + token: "Jhj5dZrVaK7ZwHHjRyZWjbDl", + team_id: "T024BE7LD", + api_app_id: "A0MDYCDME", + event: { + type: "message", + user: "U061F7AUR", + text: "Deploy finished, all green.", + ts: "1355517523.000005", + channel: "C0T2GACDT", + channel_type: "channel", + event_ts: "1355517523.000005", + }, + type: "event_callback", + authorizations: [ + { + team_id: "T024BE7LD", + user_id: "U0LAN0Z89", + is_bot: true, + is_enterprise_install: false, + }, + ], + event_id: "Ev0MDYA7HM", + event_time: 1355517523, + }, + docsUrl: "https://docs.slack.dev/reference/events/message/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "slack", + providerLabel: "Slack", + eventType: "reaction_added", + name: "Reaction added", + description: "A user added an emoji reaction to a message.", + body: { + token: "Jhj5dZrVaK7ZwHHjRyZWjbDl", + team_id: "T024BE7LD", + api_app_id: "A0MDYCDME", + event: { + type: "reaction_added", + user: "U061F7AUR", + reaction: "thumbsup", + item_user: "U0LAN0Z89", + item: { + type: "message", + channel: "C0T2GACDT", + ts: "1360782400.498405", + channel_type: "channel", + }, + event_ts: "1360782804.083113", + }, + type: "event_callback", + authorizations: [ + { + team_id: "T024BE7LD", + user_id: "U0LAN0Z89", + is_bot: true, + is_enterprise_install: false, + }, + ], + event_id: "Ev0MDYA7HN", + event_time: 1360782804, + }, + docsUrl: "https://docs.slack.dev/reference/events/reaction_added/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "slack", + providerLabel: "Slack", + eventType: "member_joined_channel", + name: "Member joined channel", + description: "A user joined a channel the bot is watching.", + body: { + token: "Jhj5dZrVaK7ZwHHjRyZWjbDl", + team_id: "T024BE7LD", + api_app_id: "A0MDYCDME", + event: { + type: "member_joined_channel", + user: "W061F7AUR", + channel: "C0T2GACDT", + channel_type: "C", + team: "T024BE7LD", + inviter: "U0LAN0Z89", + }, + type: "event_callback", + authorizations: [ + { + team_id: "T024BE7LD", + user_id: "U0LAN0Z89", + is_bot: true, + is_enterprise_install: false, + }, + ], + event_id: "Ev0MDYA7HP", + event_time: 1515500000, + }, + docsUrl: "https://docs.slack.dev/reference/events/member_joined_channel/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "slack", + providerLabel: "Slack", + eventType: "app_home_opened", + name: "App home opened", + description: "A user opened the bot's Home tab in the Slack app.", + body: { + token: "Jhj5dZrVaK7ZwHHjRyZWjbDl", + team_id: "T024BE7LD", + api_app_id: "A0MDYCDME", + event: { + type: "app_home_opened", + user: "U061F7AUR", + channel: "D0T2GACDT", + event_ts: "1515449522000016", + tab: "home", + view: { + id: "V0MDYA7HQ", + team_id: "T024BE7LD", + type: "home", + blocks: [], + private_metadata: "", + callback_id: "", + hash: "1231232323.12321312", + clear_on_close: false, + notify_on_close: false, + root_view_id: "V0MDYA7HQ", + app_id: "A0MDYCDME", + external_id: "", + app_installed_team_id: "T024BE7LD", + bot_id: "B0MDYCDMF", + }, + }, + type: "event_callback", + authorizations: [ + { + team_id: "T024BE7LD", + user_id: "U0LAN0Z89", + is_bot: true, + is_enterprise_install: false, + }, + ], + event_id: "Ev0MDYA7HQ", + event_time: 1515449522, + }, + docsUrl: "https://docs.slack.dev/reference/events/app_home_opened/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/square.ts b/internal-packages/webhook-sources/src/handAuthored/square.ts new file mode 100644 index 00000000000..a9594ab58a6 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/square.ts @@ -0,0 +1,203 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Square samples. Square signs webhooks with an HMAC-SHA-256 signature over the notification URL + * concatenated with the raw body, base64-encoded, in the `x-square-hmacsha256-signature` header - + * this matches our `square` preset exactly, so `presetId: "square"` keeps them under the round-trip + * guarantee. + */ +export const samples: SampleRecord[] = [ + { + provider: "square", + providerLabel: "Square", + presetId: "square", + eventType: "payment.created", + name: "Payment created", + description: "A new payment was created, in an APPROVED (not yet captured) state.", + body: { + merchant_id: "6SSW7HV8K2ST5", + type: "payment.created", + event_id: "13b867cf-db3d-4b1c-90b6-2f32a9d78124", + created_at: "2026-07-01T21:27:30.792Z", + data: { + type: "payment", + id: "hYy9pRFVxpDsO1FB05SunFWUe9JZY", + object: { + payment: { + id: "hYy9pRFVxpDsO1FB05SunFWUe9JZY", + created_at: "2026-07-01T21:16:51.086Z", + updated_at: "2026-07-01T21:16:51.198Z", + amount_money: { amount: 100, currency: "USD" }, + status: "APPROVED", + delay_duration: "PT168H", + source_type: "CARD", + card_details: { + status: "AUTHORIZED", + card: { + card_brand: "MASTERCARD", + last_4: "9029", + exp_month: 11, + exp_year: 2028, + fingerprint: + "sq-1-Tvruf3vPQxlvI6n0IcKYfBukrcv6IqWr8UyBdViWXU2yzGn5VMJvrsHMKpINMhPmVg", + card_type: "CREDIT", + prepaid_type: "NOT_PREPAID", + bin: "540988", + }, + entry_method: "KEYED", + cvv_status: "CVV_ACCEPTED", + avs_status: "AVS_ACCEPTED", + statement_description: "SQ *DEFAULT TEST ACCOUNT", + card_payment_timeline: { authorized_at: "2026-07-01T21:16:51.198Z" }, + }, + location_id: "S8GWD5R9QB376", + order_id: "03O3USaPaAaFnI6kkwB1JxGgBsUZY", + risk_evaluation: { + created_at: "2026-07-01T21:16:51.198Z", + risk_level: "NORMAL", + }, + total_money: { amount: 100, currency: "USD" }, + approved_money: { amount: 100, currency: "USD" }, + capabilities: ["EDIT_TIP_AMOUNT", "EDIT_TIP_AMOUNT_UP", "EDIT_TIP_AMOUNT_DOWN"], + receipt_number: "hYy9", + delay_action: "CANCEL", + delayed_until: "2026-07-08T21:16:51.086Z", + version_token: "FfQhQJf9r3VSQIgyWBk1oqhIwiznLwVwJbVVA0bdyEv6o", + }, + }, + }, + }, + docsUrl: + "https://developer.squareup.com/reference/square/payments-api/webhooks/payment.created", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "square", + providerLabel: "Square", + presetId: "square", + eventType: "payment.updated", + name: "Payment updated", + description: "A payment transitioned to COMPLETED after capture.", + body: { + merchant_id: "6SSW7HV8K2ST5", + type: "payment.updated", + event_id: "6a8f5f28-54a1-4eb0-a98a-3111513fd4fc", + created_at: "2026-07-01T21:27:34.308Z", + data: { + type: "payment", + id: "hYy9pRFVxpDsO1FB05SunFWUe9JZY", + object: { + payment: { + id: "hYy9pRFVxpDsO1FB05SunFWUe9JZY", + created_at: "2026-07-01T21:16:51.086Z", + updated_at: "2026-07-01T21:19:00.831Z", + amount_money: { amount: 100, currency: "USD" }, + status: "COMPLETED", + delay_duration: "PT168H", + source_type: "CARD", + card_details: { + status: "CAPTURED", + card: { + card_brand: "MASTERCARD", + last_4: "9029", + exp_month: 11, + exp_year: 2028, + fingerprint: + "sq-1-Tvruf3vPQxlvI6n0IcKYfBukrcv6IqWr8UyBdViWXU2yzGn5VMJvrsHMKpINMhPmVg", + card_type: "CREDIT", + prepaid_type: "NOT_PREPAID", + bin: "540988", + }, + entry_method: "KEYED", + cvv_status: "CVV_ACCEPTED", + avs_status: "AVS_ACCEPTED", + statement_description: "SQ *DEFAULT TEST ACCOUNT", + card_payment_timeline: { + authorized_at: "2026-07-01T21:16:51.198Z", + captured_at: "2026-07-01T21:19:00.832Z", + }, + }, + location_id: "S8GWD5R9QB376", + order_id: "03O3USaPaAaFnI6kkwB1JxGgBsUZY", + risk_evaluation: { + created_at: "2026-07-01T21:16:51.198Z", + risk_level: "NORMAL", + }, + total_money: { amount: 100, currency: "USD" }, + approved_money: { amount: 100, currency: "USD" }, + receipt_number: "hYy9", + receipt_url: "https://squareup.com/receipt/preview/hYy9pRFVxpDsO1FB05SunFWUe9JZY", + delay_action: "CANCEL", + delayed_until: "2026-07-08T21:16:51.086Z", + version_token: "bhC3b8qKJvNDdxqKzXaeDsAjS1oMFuAKxGgT32HbE6S6o", + }, + }, + }, + }, + docsUrl: + "https://developer.squareup.com/reference/square/payments-api/webhooks/payment.updated", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "square", + providerLabel: "Square", + presetId: "square", + eventType: "order.created", + name: "Order created", + description: "A new order was created via the Orders API.", + body: { + merchant_id: "5S9MXCS9Y99KK", + type: "order.created", + event_id: "116038d3-2948-439f-8679-fc86dbf80f69", + created_at: "2026-07-01T23:14:26.129Z", + data: { + type: "order_created", + id: "eA3vssLHKJrv9H0IdJCM3gNqfdcZY", + object: { + order_created: { + created_at: "2026-07-01T23:14:26.129Z", + location_id: "FPYCBCHYMXFK1", + order_id: "eA3vssLHKJrv9H0IdJCM3gNqfdcZY", + state: "OPEN", + version: 1, + }, + }, + }, + }, + docsUrl: "https://developer.squareup.com/reference/square/orders-api/webhooks/order.created", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "square", + providerLabel: "Square", + presetId: "square", + eventType: "refund.created", + name: "Refund created", + description: "A refund was created against a prior payment and is pending settlement.", + body: { + merchant_id: "6SSW7HV8K2ST5", + type: "refund.created", + event_id: "bc316346-6691-4243-88ed-6d651a0d0c47", + created_at: "2026-07-01T21:27:41.852Z", + data: { + type: "refund", + id: "KkAkhdMsgzn59SM8A89WgKwekxLZY_ptNBVqHYxt5gAdfcobBe4u1AZsXhoz06KTtuq9Ls24P", + object: { + refund: { + id: "KkAkhdMsgzn59SM8A89WgKwekxLZY_ptNBVqHYxt5gAdfcobBe4u1AZsXhoz06KTtuq9Ls24P", + created_at: "2026-07-01T21:27:41.836Z", + updated_at: "2026-07-01T21:27:41.846Z", + amount_money: { amount: 1000, currency: "USD" }, + status: "PENDING", + location_id: "NAQ1FHV6ZJ8YV", + order_id: "haOyDuHiqtAXMk0d8pDKXpL7Jg4F", + payment_id: "KkAkhdMsgzn59SM8A89WgKwekxLZY", + version: 7, + }, + }, + }, + }, + docsUrl: "https://developer.squareup.com/reference/square/refunds-api/webhooks/refund.created", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/supabase.ts b/internal-packages/webhook-sources/src/handAuthored/supabase.ts new file mode 100644 index 00000000000..1cad07522ee --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/supabase.ts @@ -0,0 +1,147 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Supabase Database Webhook samples. Supabase has no built-in signing scheme (verification is + * whatever custom header the integrator wires into the underlying `net.http_post` trigger), so no + * `presetId` is set on any sample here. `eventType` is the body's top-level `type` field + * (`INSERT` / `UPDATE` / `DELETE`); `record` holds the new row and `old_record` the previous row, + * with one or the other `null` depending on the operation. + */ +export const samples: SampleRecord[] = [ + { + provider: "supabase", + providerLabel: "Supabase", + eventType: "INSERT", + name: "Order created", + description: "A new row was inserted into the public.orders table.", + body: { + type: "INSERT", + table: "orders", + schema: "public", + record: { + id: 4821, + customer_id: "b1a2c3d4-5e6f-47a8-9b0c-1d2e3f4a5b6c", + status: "pending", + total_cents: 8999, + currency: "usd", + created_at: "2026-07-14T18:22:03.511Z", + updated_at: "2026-07-14T18:22:03.511Z", + }, + old_record: null, + }, + docsUrl: "https://supabase.com/docs/guides/database/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "supabase", + providerLabel: "Supabase", + eventType: "UPDATE", + name: "Order status updated", + description: + "An existing public.orders row moved from pending to shipped. `old_record` requires REPLICA IDENTITY FULL on the table, otherwise it is null.", + body: { + type: "UPDATE", + table: "orders", + schema: "public", + record: { + id: 4821, + customer_id: "b1a2c3d4-5e6f-47a8-9b0c-1d2e3f4a5b6c", + status: "shipped", + total_cents: 8999, + currency: "usd", + created_at: "2026-07-14T18:22:03.511Z", + updated_at: "2026-07-15T09:41:27.204Z", + }, + old_record: { + id: 4821, + customer_id: "b1a2c3d4-5e6f-47a8-9b0c-1d2e3f4a5b6c", + status: "pending", + total_cents: 8999, + currency: "usd", + created_at: "2026-07-14T18:22:03.511Z", + updated_at: "2026-07-14T18:22:03.511Z", + }, + }, + docsUrl: "https://supabase.com/docs/guides/database/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "supabase", + providerLabel: "Supabase", + eventType: "DELETE", + name: "Order deleted", + description: + "A public.orders row was deleted; `record` is null and `old_record` is the removed row.", + body: { + type: "DELETE", + table: "orders", + schema: "public", + record: null, + old_record: { + id: 4790, + customer_id: "f3e2d1c0-8b7a-4693-8c5d-2a1b0c9d8e7f", + status: "cancelled", + total_cents: 4500, + currency: "usd", + created_at: "2026-07-11T12:05:44.883Z", + updated_at: "2026-07-13T07:18:52.036Z", + }, + }, + docsUrl: "https://supabase.com/docs/guides/database/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "supabase", + providerLabel: "Supabase", + eventType: "INSERT", + name: "Profile created", + description: + "A new row was inserted into public.profiles, a common pattern for mirroring auth.users signups into an app-owned table.", + body: { + type: "INSERT", + table: "profiles", + schema: "public", + record: { + id: "c7d8e9f0-1a2b-43c4-8d5e-6f7a8b9c0d1e", + username: "jordan_lee", + full_name: "Jordan Lee", + avatar_url: + "https://xyzcompany.supabase.co/storage/v1/object/public/avatars/jordan_lee.png", + created_at: "2026-07-15T08:03:11.902Z", + }, + old_record: null, + }, + docsUrl: "https://supabase.com/docs/guides/database/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "supabase", + providerLabel: "Supabase", + eventType: "UPDATE", + name: "Profile updated", + description: "A public.profiles row changed its username.", + body: { + type: "UPDATE", + table: "profiles", + schema: "public", + record: { + id: "c7d8e9f0-1a2b-43c4-8d5e-6f7a8b9c0d1e", + username: "jordan.lee", + full_name: "Jordan Lee", + avatar_url: + "https://xyzcompany.supabase.co/storage/v1/object/public/avatars/jordan_lee.png", + created_at: "2026-07-15T08:03:11.902Z", + }, + old_record: { + id: "c7d8e9f0-1a2b-43c4-8d5e-6f7a8b9c0d1e", + username: "jordan_lee", + full_name: "Jordan Lee", + avatar_url: + "https://xyzcompany.supabase.co/storage/v1/object/public/avatars/jordan_lee.png", + created_at: "2026-07-15T08:03:11.902Z", + }, + }, + docsUrl: "https://supabase.com/docs/guides/database/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/telegram.ts b/internal-packages/webhook-sources/src/handAuthored/telegram.ts new file mode 100644 index 00000000000..11cf9706994 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/telegram.ts @@ -0,0 +1,146 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Telegram Bot API Update samples. An Update carries exactly one of several optional keys (message, + * edited_message, callback_query, inline_query, ...) and that key name IS the event type - there is no + * separate `type` field. Auth is a static shared secret compared against the + * X-Telegram-Bot-Api-Secret-Token header (set via `secret_token` on `setWebhook`), not an HMAC/Ed25519 + * signature, so these ship sample-only (no `presetId`). + */ +export const samples: SampleRecord[] = [ + { + provider: "telegram", + providerLabel: "Telegram", + eventType: "message", + name: "Message", + description: "An incoming text message in a private chat.", + body: { + update_id: 397587257, + message: { + message_id: 486, + from: { + id: 927485618, + is_bot: false, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + language_code: "en", + }, + chat: { + id: 927485618, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + type: "private", + }, + date: 1752562800, + text: "Hey, is the shipment on track for Friday?", + }, + }, + docsUrl: "https://core.telegram.org/bots/api#setwebhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "telegram", + providerLabel: "Telegram", + eventType: "edited_message", + name: "Edited message", + description: "The sender edited a previously sent message. `edit_date` marks when.", + body: { + update_id: 397587258, + edited_message: { + message_id: 486, + from: { + id: 927485618, + is_bot: false, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + language_code: "en", + }, + chat: { + id: 927485618, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + type: "private", + }, + date: 1752562800, + edit_date: 1752562845, + text: "Hey, is the shipment on track for next Friday?", + }, + }, + docsUrl: "https://core.telegram.org/bots/api#setwebhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "telegram", + providerLabel: "Telegram", + eventType: "callback_query", + name: "Callback query", + description: + "A user tapped an inline keyboard button. `data` is the button's opaque payload; `chat_instance` scopes the tap to the chat it happened in.", + body: { + update_id: 397587259, + callback_query: { + id: "4382174950123456789", + from: { + id: 927485618, + is_bot: false, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + language_code: "en", + }, + message: { + message_id: 490, + from: { + id: 6473920185, + is_bot: true, + first_name: "OrderStatusBot", + username: "order_status_bot", + }, + chat: { + id: 927485618, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + type: "private", + }, + date: 1752563100, + text: "Choose a shipment to track:", + }, + chat_instance: "-4827193650918273645", + data: "track_shipment:SHP-58213", + }, + }, + docsUrl: "https://core.telegram.org/bots/api#setwebhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "telegram", + providerLabel: "Telegram", + eventType: "inline_query", + name: "Inline query", + description: + "The user typed `@botname ...` in any chat; `query` is the text typed after the mention.", + body: { + update_id: 397587260, + inline_query: { + id: "1836452901847362910", + from: { + id: 927485618, + is_bot: false, + first_name: "Priya", + last_name: "Nair", + username: "priyanair", + language_code: "en", + }, + query: "shipment SHP-58213", + offset: "", + }, + }, + docsUrl: "https://core.telegram.org/bots/api#setwebhook", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/twilio.ts b/internal-packages/webhook-sources/src/handAuthored/twilio.ts new file mode 100644 index 00000000000..a57fba5482d --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/twilio.ts @@ -0,0 +1,152 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Twilio samples. Requests are `application/x-www-form-urlencoded`, decoded here as a flat string + * key/value object (Twilio's own wire format has no nesting and every value is a string, including + * numeric-looking ones like `NumMedia`/`CallDuration`). No `presetId`: Twilio's `X-Twilio-Signature` + * scheme (HMAC-SHA1 over the webhook URL + sorted params, base64) does not match any of our verifier + * presets. Covers the two products that hit a Twilio webhook URL: inbound Messaging (SMS/MMS) and Voice + * (the initial call request plus status-callback requests). + */ +export const samples: SampleRecord[] = [ + { + provider: "twilio", + providerLabel: "Twilio", + eventType: "message.inbound", + name: "Inbound SMS", + description: + "A text message received by a Twilio number. `Body` is the message text; `SmsStatus` is always `received` for inbound messages.", + body: { + ToCountry: "US", + ToState: "CA", + SmsMessageSid: "SMe1e2b7c8f5a34d6d8b8c1a2b3c4d5e60", + NumMedia: "0", + ToCity: "SAN FRANCISCO", + FromZip: "94107", + FromState: "CA", + SmsStatus: "received", + FromCity: "SAN FRANCISCO", + Body: "Running about 10 min late, on my way!", + FromCountry: "US", + To: "+14155550142", + MessagingServiceSid: "MG9f8e7d6c5b4a3928f1e0d9c8b7a6f5e4", + ToZip: "94107", + NumSegments: "1", + MessageSid: "SMe1e2b7c8f5a34d6d8b8c1a2b3c4d5e60", + AccountSid: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + SmsSid: "SMe1e2b7c8f5a34d6d8b8c1a2b3c4d5e60", + From: "+14155550198", + ApiVersion: "2010-04-01", + }, + docsUrl: "https://www.twilio.com/docs/messaging/guides/webhook-request", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "twilio", + providerLabel: "Twilio", + eventType: "message.inbound-mms", + name: "Inbound MMS", + description: + "A picture message received by a Twilio number. `NumMedia` is non-zero and each attachment is described by an indexed `MediaUrl{N}`/`MediaContentType{N}` pair.", + body: { + ToCountry: "US", + ToState: "CA", + SmsMessageSid: "MM2b3c4d5e6f7890a1b2c3d4e5f6071829", + NumMedia: "1", + ToCity: "SAN FRANCISCO", + FromZip: "94107", + FromState: "CA", + SmsStatus: "received", + FromCity: "SAN FRANCISCO", + Body: "Here's the receipt", + FromCountry: "US", + To: "+14155550142", + MessagingServiceSid: "MG9f8e7d6c5b4a3928f1e0d9c8b7a6f5e4", + ToZip: "94107", + NumSegments: "1", + MessageSid: "MM2b3c4d5e6f7890a1b2c3d4e5f6071829", + AccountSid: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + SmsSid: "MM2b3c4d5e6f7890a1b2c3d4e5f6071829", + From: "+14155550198", + ApiVersion: "2010-04-01", + MediaContentType0: "image/jpeg", + MediaUrl0: + "https://api.twilio.com/2010-04-01/Accounts/ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages/MM2b3c4d5e6f7890a1b2c3d4e5f6071829/Media/ME3c4d5e6f7890a1b2c3d4e5f60718293", + }, + docsUrl: "https://www.twilio.com/docs/messaging/guides/webhook-request", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "twilio", + providerLabel: "Twilio", + eventType: "call.inbound", + name: "Incoming call", + description: + "Twilio's request to the voice webhook URL when a call comes in. `CallStatus` is `ringing`; the app answers with TwiML in the response.", + body: { + CallSid: "CA3f9e8d7c6b5a4938271605f4e3d2c1b0a", + AccountSid: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + From: "+14155550198", + To: "+14155550142", + CallStatus: "ringing", + ApiVersion: "2010-04-01", + Direction: "inbound", + FromCity: "SAN FRANCISCO", + FromState: "CA", + FromZip: "94107", + FromCountry: "US", + ToCity: "SAN FRANCISCO", + ToState: "CA", + ToZip: "94107", + ToCountry: "US", + }, + docsUrl: "https://www.twilio.com/docs/voice/twiml#twilios-request-to-your-application", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "twilio", + providerLabel: "Twilio", + eventType: "call.status-callback", + name: "Call status callback: completed", + description: + "A voice `StatusCallback` request after the call ended normally. `CallDuration` (seconds) is only present once `CallStatus` reaches a terminal value.", + body: { + CallSid: "CA3f9e8d7c6b5a4938271605f4e3d2c1b0a", + AccountSid: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + From: "+14155550198", + To: "+14155550142", + CallStatus: "completed", + ApiVersion: "2010-04-01", + Direction: "inbound", + CallDuration: "187", + Timestamp: "Wed, 15 Jul 2026 14:32:18 +0000", + CallbackSource: "call-progress-events", + SequenceNumber: "3", + SipResponseCode: "200", + }, + docsUrl: "https://www.twilio.com/docs/usage/webhooks/voice-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "twilio", + providerLabel: "Twilio", + eventType: "call.status-callback", + name: "Call status callback: no-answer", + description: + "A voice `StatusCallback` request for an outbound call nobody picked up. No `CallDuration` is sent for non-`completed` terminal statuses.", + body: { + CallSid: "CA7c6b5a4938271605f4e3d2c1b0a9f8e7d", + AccountSid: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + From: "+14155550142", + To: "+14155550287", + CallStatus: "no-answer", + ApiVersion: "2010-04-01", + Direction: "outbound-api", + Timestamp: "Wed, 15 Jul 2026 09:04:52 +0000", + CallbackSource: "call-progress-events", + SequenceNumber: "2", + }, + docsUrl: "https://www.twilio.com/docs/usage/webhooks/voice-webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/typeform.ts b/internal-packages/webhook-sources/src/handAuthored/typeform.ts new file mode 100644 index 00000000000..e230fc70d4b --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/typeform.ts @@ -0,0 +1,321 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Typeform samples. Typeform has a single webhook event, `form_response`, fired whenever a respondent + * completes a form; these vary the form shape (field types, hidden fields, calculated score) to cover + * the range of `definition.fields` / `answers` combinations integrators actually see. Typeform signs + * with a `Typeform-Signature: sha256=` header over the raw body, which doesn't match any of our + * verifier presets, so no `presetId` is set. + */ +export const samples: SampleRecord[] = [ + { + provider: "typeform", + providerLabel: "Typeform", + eventType: "form_response", + name: "Contact form submission", + description: "A respondent completed a basic contact form (name, email, message).", + body: { + event_id: "01HXQ3ZK9BCONTACT01", + event_type: "form_response", + form_response: { + form_id: "A1b2C3", + token: "6f3e8a1c9d2b47a0b6e5f1a2c3d4e5f6", + response_id: "6f3e8a1c9d2b47a0b6e5f1a2c3d4e5f6", + response_url: + "https://admin.typeform.com/form/A1b2C3/results?responseId=6f3e8a1c9d2b47a0b6e5f1a2c3d4e5f6", + landed_at: "2026-07-08T14:02:11Z", + submitted_at: "2026-07-08T14:04:47Z", + hidden: { + utm_source: "newsletter", + }, + definition: { + id: "A1b2C3", + title: "Contact us", + fields: [ + { + id: "JwWggjAKtOk1", + title: "What's your full name?", + type: "short_text", + ref: "full_name", + }, + { + id: "JwWggjAKtOk2", + title: "What's your email address?", + type: "email", + ref: "email", + }, + { id: "JwWggjAKtOk3", title: "How can we help?", type: "long_text", ref: "message" }, + ], + }, + answers: [ + { + type: "text", + text: "Priya Nair", + field: { id: "JwWggjAKtOk1", type: "short_text", ref: "full_name" }, + }, + { + type: "email", + email: "priya.nair@example.com", + field: { id: "JwWggjAKtOk2", type: "email", ref: "email" }, + }, + { + type: "text", + text: "Hi, I'd like a quote for the annual plan for a 40-person team.", + field: { id: "JwWggjAKtOk3", type: "long_text", ref: "message" }, + }, + ], + }, + }, + docsUrl: "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "typeform", + providerLabel: "Typeform", + eventType: "form_response", + name: "Customer satisfaction survey", + description: + "A CSAT/NPS survey response with a scored opinion-scale answer; `calculated.score` reflects the form's scoring configuration.", + body: { + event_id: "01HXQ3ZK9BCSAT0001", + event_type: "form_response", + form_response: { + form_id: "D4e5F6", + token: "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d", + response_id: "9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d", + response_url: + "https://admin.typeform.com/form/D4e5F6/results?responseId=9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d", + landed_at: "2026-07-09T09:11:02Z", + submitted_at: "2026-07-09T09:12:30Z", + calculated: { + score: 9, + }, + hidden: { + user_id: "cus_9f8e7d6c5b4a", + plan: "growth", + }, + definition: { + id: "D4e5F6", + title: "How are we doing?", + fields: [ + { + id: "Q7M2XAwY04d1", + title: "How likely are you to recommend us to a friend?", + type: "opinion_scale", + ref: "nps_score", + }, + { + id: "Q7M2XAwY04d2", + title: "Anything you'd like us to improve?", + type: "long_text", + ref: "improve_feedback", + }, + ], + }, + answers: [ + { + type: "number", + number: 9, + field: { id: "Q7M2XAwY04d1", type: "opinion_scale", ref: "nps_score" }, + }, + { + type: "text", + text: "The mobile app could load a bit faster on older devices.", + field: { id: "Q7M2XAwY04d2", type: "long_text", ref: "improve_feedback" }, + }, + ], + }, + }, + docsUrl: "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "typeform", + providerLabel: "Typeform", + eventType: "form_response", + name: "Job application", + description: + "A job application with a phone number, a file upload, and a single-choice referral field.", + body: { + event_id: "01HXQ3ZK9BJOB00001", + event_type: "form_response", + form_response: { + form_id: "G7h8I9", + token: "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", + response_id: "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", + response_url: + "https://admin.typeform.com/form/G7h8I9/results?responseId=1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d", + landed_at: "2026-07-10T16:40:05Z", + submitted_at: "2026-07-10T16:48:22Z", + hidden: { + req_id: "req_284910", + }, + definition: { + id: "G7h8I9", + title: "Senior Support Engineer application", + fields: [ + { id: "KpNvQ2rTsUx1", title: "Full name", type: "short_text", ref: "full_name" }, + { id: "KpNvQ2rTsUx2", title: "Phone number", type: "phone_number", ref: "phone" }, + { id: "KpNvQ2rTsUx3", title: "Upload your resume", type: "file_upload", ref: "resume" }, + { + id: "KpNvQ2rTsUx4", + title: "How did you hear about us?", + type: "multiple_choice", + ref: "referral_source", + }, + ], + }, + answers: [ + { + type: "text", + text: "Sam Okafor", + field: { id: "KpNvQ2rTsUx1", type: "short_text", ref: "full_name" }, + }, + { + type: "phone_number", + phone_number: "+15551234567", + field: { id: "KpNvQ2rTsUx2", type: "phone_number", ref: "phone" }, + }, + { + type: "file_url", + file_url: + "https://api.typeform.com/forms/G7h8I9/responses/1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/files/KpNvQ2rTsUx3/0/samokafor-resume.pdf", + field: { id: "KpNvQ2rTsUx3", type: "file_upload", ref: "resume" }, + }, + { + type: "choice", + choice: { label: "LinkedIn" }, + field: { id: "KpNvQ2rTsUx4", type: "multiple_choice", ref: "referral_source" }, + }, + ], + }, + }, + docsUrl: "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "typeform", + providerLabel: "Typeform", + eventType: "form_response", + name: "Event registration", + description: + "An event RSVP with a single-choice ticket field, a multi-select dietary field, and a legal consent checkbox.", + body: { + event_id: "01HXQ3ZK9BEVT00001", + event_type: "form_response", + form_response: { + form_id: "J1k2L3", + token: "5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + response_id: "5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + response_url: + "https://admin.typeform.com/form/J1k2L3/results?responseId=5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b", + landed_at: "2026-07-11T11:03:44Z", + submitted_at: "2026-07-11T11:06:19Z", + hidden: { + campaign: "summit-2026", + }, + definition: { + id: "J1k2L3", + title: "Register for the 2026 Summit", + fields: [ + { + id: "MqRwT4vXyZa1", + title: "Ticket type", + type: "multiple_choice", + ref: "ticket_type", + }, + { + id: "MqRwT4vXyZa2", + title: "Any dietary restrictions?", + type: "multiple_choice", + ref: "dietary_restrictions", + }, + { + id: "MqRwT4vXyZa3", + title: "I agree to the event terms", + type: "legal", + ref: "agree_terms", + }, + ], + }, + answers: [ + { + type: "choice", + choice: { label: "General admission" }, + field: { id: "MqRwT4vXyZa1", type: "multiple_choice", ref: "ticket_type" }, + }, + { + type: "choices", + choices: { labels: ["Vegetarian", "Nut allergy"] }, + field: { id: "MqRwT4vXyZa2", type: "multiple_choice", ref: "dietary_restrictions" }, + }, + { + type: "boolean", + boolean: true, + field: { id: "MqRwT4vXyZa3", type: "legal", ref: "agree_terms" }, + }, + ], + }, + }, + docsUrl: "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "typeform", + providerLabel: "Typeform", + eventType: "form_response", + name: "Product waitlist signup", + description: + "A waitlist signup with an email, a dropdown for company size, and a target-launch date.", + body: { + event_id: "01HXQ3ZK9BWAIT0001", + event_type: "form_response", + form_response: { + form_id: "N4o5P6", + token: "3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f", + response_id: "3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f", + response_url: + "https://admin.typeform.com/form/N4o5P6/results?responseId=3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f", + landed_at: "2026-07-12T08:20:00Z", + submitted_at: "2026-07-12T08:21:15Z", + hidden: { + utm_source: "product-hunt", + utm_campaign: "launch-waitlist", + }, + definition: { + id: "N4o5P6", + title: "Join the waitlist", + fields: [ + { id: "RtYuI7oPaSd1", title: "Work email", type: "email", ref: "work_email" }, + { id: "RtYuI7oPaSd2", title: "Company size", type: "dropdown", ref: "company_size" }, + { + id: "RtYuI7oPaSd3", + title: "When do you want to launch?", + type: "date", + ref: "target_launch_date", + }, + ], + }, + answers: [ + { + type: "email", + email: "jordan.lee@example.com", + field: { id: "RtYuI7oPaSd1", type: "email", ref: "work_email" }, + }, + { + type: "choice", + choice: { label: "11-50" }, + field: { id: "RtYuI7oPaSd2", type: "dropdown", ref: "company_size" }, + }, + { + type: "date", + date: "2026-09-01", + field: { id: "RtYuI7oPaSd3", type: "date", ref: "target_launch_date" }, + }, + ], + }, + }, + docsUrl: "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/vapi.ts b/internal-packages/webhook-sources/src/handAuthored/vapi.ts new file mode 100644 index 00000000000..00df728001b --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/vapi.ts @@ -0,0 +1,160 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Vapi server webhook samples. Every message is wrapped in a top-level `message` object whose `type` + * field is the event discriminant (`eventType` mirrors it here). Vapi signs with a bespoke + * `x-vapi-signature` header (raw HMAC-SHA256 hex digest of the raw body, integrator-configured secret, + * no timestamp/prefix), which does not match any of our verifier presets, so these ship sample-only + * with no `presetId`. + */ +export const samples: SampleRecord[] = [ + { + provider: "vapi", + providerLabel: "Vapi", + eventType: "status-update", + name: "Status update", + description: "The call transitioned to a new status, here from ringing to in-progress.", + body: { + message: { + type: "status-update", + status: "in-progress", + timestamp: 1783533600123, + call: { + id: "call_29a1f6a0-6b3d-4e2a-9c7d-8f5e6a2b1c40", + orgId: "org_4f6e2a91-7c3b-4d5a-9e1f-2b8c6d4a3f90", + type: "outboundPhoneCall", + status: "in-progress", + createdAt: "2026-07-14T18:59:57.201Z", + updatedAt: "2026-07-14T19:00:00.123Z", + assistantId: "asst_7d4c2b91-3a6e-4f8d-b1c5-9e2a7d6c4b30", + phoneNumberId: "pn_5b3d8a92-1c4e-4a7f-9d6b-2e8c1a4f7b60", + customer: { + number: "+14155550142", + }, + }, + }, + }, + docsUrl: "https://docs.vapi.ai/server-url/events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "vapi", + providerLabel: "Vapi", + eventType: "transcript", + name: "Transcript", + description: "A final transcript chunk from the caller's turn.", + body: { + message: { + type: "transcript", + role: "user", + transcriptType: "final", + transcript: "I need to reschedule my appointment to next Tuesday afternoon.", + call: { + id: "call_29a1f6a0-6b3d-4e2a-9c7d-8f5e6a2b1c40", + orgId: "org_4f6e2a91-7c3b-4d5a-9e1f-2b8c6d4a3f90", + type: "outboundPhoneCall", + status: "in-progress", + assistantId: "asst_7d4c2b91-3a6e-4f8d-b1c5-9e2a7d6c4b30", + customer: { + number: "+14155550142", + }, + }, + }, + }, + docsUrl: "https://docs.vapi.ai/server-url/events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "vapi", + providerLabel: "Vapi", + eventType: "tool-calls", + name: "Tool calls", + description: + "The assistant invoked a configured function tool; the server is expected to respond with the tool's result.", + body: { + message: { + type: "tool-calls", + toolCallList: [ + { + id: "tool_call_8c2d6a91-4b7e-4f3a-9d1c-6e8a2b4f7c50", + type: "function", + function: { + name: "reschedule_appointment", + arguments: '{"appointmentId":"appt_5120","newTime":"2026-07-21T14:00:00-07:00"}', + }, + }, + ], + call: { + id: "call_29a1f6a0-6b3d-4e2a-9c7d-8f5e6a2b1c40", + orgId: "org_4f6e2a91-7c3b-4d5a-9e1f-2b8c6d4a3f90", + type: "outboundPhoneCall", + status: "in-progress", + assistantId: "asst_7d4c2b91-3a6e-4f8d-b1c5-9e2a7d6c4b30", + customer: { + number: "+14155550142", + }, + }, + }, + }, + docsUrl: "https://docs.vapi.ai/server-url/events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "vapi", + providerLabel: "Vapi", + eventType: "end-of-call-report", + name: "End of call report", + description: + "Sent once the call ends, carrying the ended reason plus the full artifact (transcript, messages, recording).", + body: { + message: { + type: "end-of-call-report", + endedReason: "customer-ended-call", + durationSeconds: 187.4, + cost: 0.0932, + call: { + id: "call_29a1f6a0-6b3d-4e2a-9c7d-8f5e6a2b1c40", + orgId: "org_4f6e2a91-7c3b-4d5a-9e1f-2b8c6d4a3f90", + type: "outboundPhoneCall", + status: "ended", + createdAt: "2026-07-14T18:59:57.201Z", + updatedAt: "2026-07-14T19:03:04.612Z", + assistantId: "asst_7d4c2b91-3a6e-4f8d-b1c5-9e2a7d6c4b30", + customer: { + number: "+14155550142", + }, + }, + artifact: { + transcript: + "AI: Hi, this is Riley from Sunrise Dental. How can I help?\nUser: I need to reschedule my appointment to next Tuesday afternoon.\nAI: Sure, I've moved your appointment to Tuesday at 2pm.", + messages: [ + { + role: "assistant", + message: "Hi, this is Riley from Sunrise Dental. How can I help?", + time: 1783533601400, + endTime: 1783533604100, + }, + { + role: "user", + message: "I need to reschedule my appointment to next Tuesday afternoon.", + time: 1783533605200, + endTime: 1783533608700, + }, + { + role: "assistant", + message: "Sure, I've moved your appointment to Tuesday at 2pm.", + time: 1783533609300, + endTime: 1783533612100, + }, + ], + recording: { + stereoUrl: + "https://storage.vapi.ai/recordings/call_29a1f6a0-6b3d-4e2a-9c7d-8f5e6a2b1c40.wav", + }, + }, + }, + }, + docsUrl: "https://docs.vapi.ai/server-url/events", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/vercel.ts b/internal-packages/webhook-sources/src/handAuthored/vercel.ts new file mode 100644 index 00000000000..43254a9badb --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/vercel.ts @@ -0,0 +1,192 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Vercel account/integration webhook samples. Every event shares the envelope + * `{ id, type, createdAt, region, payload }`; `type` is the discriminant. Vercel signs with a bespoke + * `x-vercel-signature` HMAC-SHA1-over-raw-body scheme (no preset match), so no `presetId` is set on + * any sample here. `payload.deployment.meta` is documented only as an opaque metadata map; the values + * below model the common git-integration keys Vercel populates for a GitHub-linked project. + */ +export const samples: SampleRecord[] = [ + { + provider: "vercel", + providerLabel: "Vercel", + eventType: "deployment.created", + name: "Deployment created", + description: "A new deployment was initiated, e.g. from a git push or `vercel deploy`.", + body: { + id: "5FQ3bE5aQhZ1Vv2c4vXYzP1LhqoT", + type: "deployment.created", + createdAt: 1752520920123, + region: null, + payload: { + team: { id: "team_LLHUOMOoDlqOp8wPE4kFo9pE" }, + user: { id: "K4amb7K9dhQC1c3Vg6M4VZur" }, + alias: ["storefront.vercel.app", "storefront-git-main-acme.vercel.app"], + deployment: { + id: "dpl_5m3gYqz6JhKGLnvGGqSpVsXQ2vHz", + meta: { + githubCommitAuthorLogin: "priyanair", + githubCommitMessage: "fix: correct cart subtotal rounding", + githubCommitOrg: "acme", + githubCommitRef: "main", + githubCommitRepo: "storefront", + githubCommitRepoId: "612345678", + githubCommitSha: "8f3a1c2e9b4d7f6a2c8e1b5d9a3f7c2e6b8d4a1f", + githubDeployment: "1", + githubOrg: "acme", + githubRepo: "storefront", + githubRepoOwnerType: "organization", + }, + url: "storefront-git-main-acme.vercel.app", + name: "storefront", + }, + links: { + deployment: "https://vercel.com/acme/storefront/5m3gYqz6JhKGLnvGGqSpVsXQ2vHz", + project: "https://vercel.com/acme/storefront", + }, + target: "production", + project: { id: "prj_8Xc1kQnR3vTz9YbW2fA6mLhE" }, + plan: "pro", + regions: ["iad1"], + }, + }, + docsUrl: "https://vercel.com/docs/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "vercel", + providerLabel: "Vercel", + eventType: "deployment.succeeded", + name: "Deployment succeeded", + description: "A deployment finished building and all blocking checks passed.", + body: { + id: "9Kd2wR7fXmQ3Vv8c5bYpZ4LhqrN", + type: "deployment.succeeded", + createdAt: 1752520998456, + region: null, + payload: { + team: { id: "team_LLHUOMOoDlqOp8wPE4kFo9pE" }, + user: { id: "K4amb7K9dhQC1c3Vg6M4VZur" }, + deployment: { + id: "dpl_5m3gYqz6JhKGLnvGGqSpVsXQ2vHz", + meta: { + githubCommitAuthorLogin: "priyanair", + githubCommitMessage: "fix: correct cart subtotal rounding", + githubCommitOrg: "acme", + githubCommitRef: "main", + githubCommitRepo: "storefront", + githubCommitRepoId: "612345678", + githubCommitSha: "8f3a1c2e9b4d7f6a2c8e1b5d9a3f7c2e6b8d4a1f", + githubDeployment: "1", + githubOrg: "acme", + githubRepo: "storefront", + githubRepoOwnerType: "organization", + }, + url: "storefront-git-main-acme.vercel.app", + name: "storefront", + }, + links: { + deployment: "https://vercel.com/acme/storefront/5m3gYqz6JhKGLnvGGqSpVsXQ2vHz", + project: "https://vercel.com/acme/storefront", + }, + target: "production", + project: { id: "prj_8Xc1kQnR3vTz9YbW2fA6mLhE" }, + plan: "pro", + regions: ["iad1"], + }, + }, + docsUrl: "https://vercel.com/docs/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "vercel", + providerLabel: "Vercel", + eventType: "deployment.error", + name: "Deployment error", + description: "A deployment failed to build.", + body: { + id: "2Hn8pL4dWkR6Tz1c9vXmA3JhqsE", + type: "deployment.error", + createdAt: 1752521045789, + region: null, + payload: { + team: { id: "team_LLHUOMOoDlqOp8wPE4kFo9pE" }, + user: { id: "6b1e9d3a4c7f4b2e8a9d1c6f3b8e" }, + deployment: { + id: "dpl_7qXn2vBpKzGjWm4cRfLtYs9AeHd6", + meta: { + githubCommitAuthorLogin: "marcuswebb", + githubCommitMessage: "feat: add checkout address autocomplete", + githubCommitOrg: "acme", + githubCommitRef: "feature/checkout-autocomplete", + githubCommitRepo: "storefront", + githubCommitRepoId: "612345678", + githubCommitSha: "3c7e9b1a5d8f2c6e4a9b7d3f1c8e5a2b6d9f4c1e", + githubDeployment: "1", + githubOrg: "acme", + githubRepo: "storefront", + githubRepoOwnerType: "organization", + }, + url: "storefront-git-checkout-autocomplete-acme.vercel.app", + name: "storefront", + }, + links: { + deployment: "https://vercel.com/acme/storefront/7qXn2vBpKzGjWm4cRfLtYs9AeHd6", + project: "https://vercel.com/acme/storefront", + }, + target: null, + project: { id: "prj_8Xc1kQnR3vTz9YbW2fA6mLhE" }, + plan: "pro", + regions: ["iad1"], + }, + }, + docsUrl: "https://vercel.com/docs/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "vercel", + providerLabel: "Vercel", + eventType: "deployment.canceled", + name: "Deployment canceled", + description: "A deployment was canceled, e.g. superseded by a newer push to the same branch.", + body: { + id: "4Mp6rT9fYnQ2Vx3c7bZoW5LhqtF", + type: "deployment.canceled", + createdAt: 1752521102345, + region: null, + payload: { + team: { id: "team_LLHUOMOoDlqOp8wPE4kFo9pE" }, + user: { id: "6b1e9d3a4c7f4b2e8a9d1c6f3b8e" }, + deployment: { + id: "dpl_1bYw6qGkPzRjNm8cVfEtLs4AeXd9", + meta: { + githubCommitAuthorLogin: "marcuswebb", + githubCommitMessage: "feat: add checkout address autocomplete (v2)", + githubCommitOrg: "acme", + githubCommitRef: "feature/checkout-autocomplete", + githubCommitRepo: "storefront", + githubCommitRepoId: "612345678", + githubCommitSha: "9a4f2c8e6b1d3a7f5c9e2b8d4a6f1c3e7b9d5a2f", + githubDeployment: "1", + githubOrg: "acme", + githubRepo: "storefront", + githubRepoOwnerType: "organization", + }, + url: "storefront-git-checkout-autocomplete-acme.vercel.app", + name: "storefront", + }, + links: { + deployment: "https://vercel.com/acme/storefront/1bYw6qGkPzRjNm8cVfEtLs4AeXd9", + project: "https://vercel.com/acme/storefront", + }, + target: null, + project: { id: "prj_8Xc1kQnR3vTz9YbW2fA6mLhE" }, + plan: "pro", + regions: ["iad1"], + }, + }, + docsUrl: "https://vercel.com/docs/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/whatsapp.ts b/internal-packages/webhook-sources/src/handAuthored/whatsapp.ts new file mode 100644 index 00000000000..43c67e9e45d --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/whatsapp.ts @@ -0,0 +1,298 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * WhatsApp Cloud API samples. Meta signs webhooks with `X-Hub-Signature-256: sha256=` + * HMAC-SHA256 over the raw body, matching `presetId: "github"` exactly. `eventType` is set to + * "messages" or "statuses" per the row hypothesis, but note this is derived from which key + * (`value.messages` vs `value.statuses`) is present in the body, not from `changes[].field` - + * that field is always the literal string "messages" in real Meta payloads, for both families. + */ +export const samples: SampleRecord[] = [ + { + provider: "whatsapp", + providerLabel: "WhatsApp (Meta)", + presetId: "github", + eventType: "messages", + name: "Inbound text message", + description: "A text message sent by a WhatsApp user to a business phone number.", + body: { + object: "whatsapp_business_account", + entry: [ + { + id: "102290129340398", + changes: [ + { + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "15550783881", + phone_number_id: "106540352242922", + }, + contacts: [{ profile: { name: "Sheena Nelson" }, wa_id: "16505551234" }], + messages: [ + { + from: "16505551234", + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTRBNjU5OUFFRTAzODEwMTQ0RgA=", + timestamp: "1751416383", + type: "text", + text: { body: "Does it come in another color?" }, + }, + ], + }, + field: "messages", + }, + ], + }, + ], + }, + docsUrl: + "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "whatsapp", + providerLabel: "WhatsApp (Meta)", + presetId: "github", + eventType: "messages", + name: "Inbound image message", + description: "An image message with a caption sent by a WhatsApp user.", + body: { + object: "whatsapp_business_account", + entry: [ + { + id: "102290129340398", + changes: [ + { + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "15550783881", + phone_number_id: "106540352242922", + }, + contacts: [{ profile: { name: "Priya Shah" }, wa_id: "16505552222" }], + messages: [ + { + from: "16505552222", + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTZBQzg0MzQ4QjRCM0NGNkVGOQA=", + timestamp: "1751426500", + type: "image", + image: { + id: "1284772819503647", + mime_type: "image/jpeg", + sha256: "k7f2mA9wQeR3vN0pLxT8jZ4hC1sD6bU5nY2iG8oV7yE=", + caption: "Here's a photo of the item", + }, + }, + ], + }, + field: "messages", + }, + ], + }, + ], + }, + docsUrl: + "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/media", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "whatsapp", + providerLabel: "WhatsApp (Meta)", + presetId: "github", + eventType: "messages", + name: "Inbound interactive button reply", + description: "A user tapping a quick-reply button from a previous business-initiated message.", + body: { + object: "whatsapp_business_account", + entry: [ + { + id: "102290129340398", + changes: [ + { + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "15550783881", + phone_number_id: "106540352242922", + }, + contacts: [{ profile: { name: "Sheena Nelson" }, wa_id: "16505551234" }], + messages: [ + { + context: { + from: "15550783881", + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBI3MEM2RUJFNkI0RENGQTVDRjUA", + }, + from: "16505551234", + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQTZBQzg0MzQ4QjRCM0NGNkVGOAA=", + timestamp: "1751425136", + type: "interactive", + interactive: { + type: "button_reply", + button_reply: { id: "cancel-order-button", title: "Cancel order" }, + }, + }, + ], + }, + field: "messages", + }, + ], + }, + ], + }, + docsUrl: + "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/interactive", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "whatsapp", + providerLabel: "WhatsApp (Meta)", + presetId: "github", + eventType: "statuses", + name: "Message status: sent", + description: + "Delivery status update after Meta accepts a business-initiated message for sending.", + body: { + object: "whatsapp_business_account", + entry: [ + { + id: "102290129340398", + changes: [ + { + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "15550783881", + phone_number_id: "106540352242922", + }, + statuses: [ + { + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMQA=", + status: "sent", + timestamp: "1751430000", + recipient_id: "16505551234", + conversation: { + id: "82c14d6bd5407799e66f64d1b338e568", + expiration_timestamp: "1751516400", + origin: { type: "service" }, + }, + pricing: { + billable: true, + pricing_model: "PMP", + type: "regular", + category: "service", + }, + }, + ], + }, + field: "messages", + }, + ], + }, + ], + }, + docsUrl: + "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "whatsapp", + providerLabel: "WhatsApp (Meta)", + presetId: "github", + eventType: "statuses", + name: "Message status: delivered", + description: "Delivery status update once the message reaches the recipient's device.", + body: { + object: "whatsapp_business_account", + entry: [ + { + id: "102290129340398", + changes: [ + { + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "15550783881", + phone_number_id: "106540352242922", + }, + statuses: [ + { + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMAA=", + status: "delivered", + timestamp: "1751430074", + recipient_id: "16505553333", + conversation: { + id: "72b14d6bd5407799e66f64d1b338e567", + expiration_timestamp: "1751516480", + origin: { type: "service" }, + }, + pricing: { + billable: true, + pricing_model: "PMP", + type: "regular", + category: "service", + }, + }, + ], + }, + field: "messages", + }, + ], + }, + ], + }, + docsUrl: + "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "whatsapp", + providerLabel: "WhatsApp (Meta)", + presetId: "github", + eventType: "statuses", + name: "Message status: failed", + description: "Delivery status update when a message fails to send, with error detail.", + body: { + object: "whatsapp_business_account", + entry: [ + { + id: "102290129340398", + changes: [ + { + value: { + messaging_product: "whatsapp", + metadata: { + display_phone_number: "15550783881", + phone_number_id: "106540352242922", + }, + statuses: [ + { + id: "wamid.HBgLMTY1MDM4Nzk0MzkVAgASGBQzQUFERjg0NDEzNDdFODU3MUMxMgA=", + status: "failed", + timestamp: "1751431301", + recipient_id: "16505554444", + errors: [ + { + code: 131026, + title: "Message undeliverable", + message: "Message undeliverable", + error_data: { + details: + "Message failed to send because more than 24 hours have passed since the customer last replied to this number.", + }, + href: "https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/", + }, + ], + }, + ], + }, + field: "messages", + }, + ], + }, + ], + }, + docsUrl: + "https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/workos.ts b/internal-packages/webhook-sources/src/handAuthored/workos.ts new file mode 100644 index 00000000000..8dff19f5502 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/workos.ts @@ -0,0 +1,167 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * WorkOS samples. WorkOS signs webhooks with its own `WorkOS-Signature: t=,v1=` scheme (HMAC-SHA256 + * over `{t}.{rawBody}`), which does not match our `stripe` preset's header name, so these ship without + * a `presetId`. Every event shares the envelope `{ id, event, data, created_at, context }`. + */ +export const samples: SampleRecord[] = [ + { + provider: "workos", + providerLabel: "WorkOS", + eventType: "dsync.user.created", + name: "Directory user created", + description: "A new user appeared in a connected Directory Sync (SCIM) provider.", + body: { + id: "event_01J4QF7R2N8T6R3P0M5NDS8WYA", + event: "dsync.user.created", + data: { + object: "directory_user", + id: "directory_user_01J4QF6VN8T6R3P0M5NDS8WYB", + directory_id: "directory_01J4QF5PM7S5R2P9M4NDS6WVC", + organization_id: "org_01J4QF4ZK6R4Q1P8M3NDS5WUD", + idp_id: "2839573", + email: "priya.nair@example.com", + first_name: "Priya", + last_name: "Nair", + emails: [{ type: "work", value: "priya.nair@example.com", primary: true }], + job_title: "Staff Engineer", + username: "priya.nair", + state: "active", + custom_attributes: { department: "Engineering" }, + raw_attributes: { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], + userName: "priya.nair", + active: true, + }, + role: { slug: "member" }, + roles: [{ slug: "member" }], + created_at: "2026-07-13T09:20:00.000Z", + updated_at: "2026-07-13T09:20:00.000Z", + }, + created_at: "2026-07-13T09:20:01.104Z", + context: { client_id: "client_01J4QF3XH5Q3P0M9NDS4WTYE" }, + }, + docsUrl: "https://workos.com/docs/events/data-syncing/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "workos", + providerLabel: "WorkOS", + eventType: "dsync.group.created", + name: "Directory group created", + description: "A new group appeared in a connected Directory Sync provider.", + body: { + id: "event_01J4QFB9P9U7S4Q1N6PDT9XZF", + event: "dsync.group.created", + data: { + object: "directory_group", + id: "directory_group_01J4QFA3Q0V8T5R2P7PDT0YAG", + idp_id: "9284756", + directory_id: "directory_01J4QF5PM7S5R2P9M4NDS6WVC", + organization_id: "org_01J4QF4ZK6R4Q1P8M3NDS5WUD", + name: "Engineering", + raw_attributes: { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], + displayName: "Engineering", + }, + created_at: "2026-07-13T09:21:15.000Z", + updated_at: "2026-07-13T09:21:15.000Z", + }, + created_at: "2026-07-13T09:21:15.892Z", + context: { client_id: "client_01J4QF3XH5Q3P0M9NDS4WTYE" }, + }, + docsUrl: "https://workos.com/docs/events/data-syncing/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "workos", + providerLabel: "WorkOS", + eventType: "connection.activated", + name: "SSO connection activated", + description: "An SSO connection finished setup and is now able to authenticate users.", + body: { + id: "event_01J4QFD2R1W9T6S3P8QEU1ZBH", + event: "connection.activated", + data: { + object: "connection", + id: "conn_01J4QFC5S2X0U7T4Q9QEU2ACJ", + organization_id: "org_01J4QF4ZK6R4Q1P8M3NDS5WUD", + connection_type: "OktaSAML", + name: "Acme Corp Okta", + state: "active", + domains: [ + { + object: "connection_domain", + id: "conn_domain_01J4QFBWT3Y1V8U5R0RFV3BDK", + domain: "acmecorp.com", + }, + ], + created_at: "2026-07-10T16:04:22.000Z", + updated_at: "2026-07-13T09:22:40.000Z", + }, + created_at: "2026-07-13T09:22:40.331Z", + context: { client_id: "client_01J4QF3XH5Q3P0M9NDS4WTYE" }, + }, + docsUrl: "https://workos.com/docs/events/data-syncing/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "workos", + providerLabel: "WorkOS", + eventType: "user.created", + name: "User created", + description: "A new AuthKit / User Management user signed up or was invited.", + body: { + id: "event_01J4QFF6T4Z2V9U6S1RFV4CEM", + event: "user.created", + data: { + object: "user", + id: "user_01J4QFE9U5A3W0V7T2SGW5DFN", + email: "jordan.lee@example.com", + email_verified: false, + first_name: "Jordan", + last_name: "Lee", + profile_picture_url: null, + external_id: null, + metadata: {}, + last_sign_in_at: null, + created_at: "2026-07-13T09:23:55.000Z", + updated_at: "2026-07-13T09:23:55.000Z", + }, + created_at: "2026-07-13T09:23:55.410Z", + context: { client_id: "client_01J4QF3XH5Q3P0M9NDS4WTYE" }, + }, + docsUrl: "https://workos.com/docs/events/data-syncing/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "workos", + providerLabel: "WorkOS", + eventType: "user.deleted", + name: "User deleted", + description: "A User Management user was permanently deleted.", + body: { + id: "event_01J4QFH8V6B4X1W8U3THX6DGP", + event: "user.deleted", + data: { + object: "user", + id: "user_01J4QFG1W7C5Y2X9V4THX7EHQ", + email: "sam.okafor@example.com", + email_verified: true, + first_name: "Sam", + last_name: "Okafor", + profile_picture_url: null, + external_id: null, + metadata: {}, + last_sign_in_at: "2026-07-10T14:02:33.000Z", + created_at: "2026-05-02T11:10:00.000Z", + updated_at: "2026-07-13T09:25:03.000Z", + }, + created_at: "2026-07-13T09:25:03.777Z", + context: { client_id: "client_01J4QF3XH5Q3P0M9NDS4WTYE" }, + }, + docsUrl: "https://workos.com/docs/events/data-syncing/webhooks", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/zendesk.ts b/internal-packages/webhook-sources/src/handAuthored/zendesk.ts new file mode 100644 index 00000000000..b709db32031 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/zendesk.ts @@ -0,0 +1,146 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Zendesk samples. Zendesk webhooks are driven by triggers/automations where an admin authors the JSON + * body from content placeholders (e.g. `{{ticket.id}}`, `{{ticket.assignee.email}}`), so there is no + * fixed payload schema across accounts - these model the common flattened "ticket" shape used in + * Zendesk's own webhook examples. No `presetId`: the signing scheme (base64 HMAC-SHA256 over + * `timestamp + body`, headers `X-Zendesk-Webhook-Signature` / `X-Zendesk-Webhook-Timestamp`) matches + * none of our presets. + */ +export const samples: SampleRecord[] = [ + { + provider: "zendesk", + providerLabel: "Zendesk", + eventType: "ticket.created", + name: "Ticket created", + description: "A new support ticket was created and is awaiting first response.", + body: { + ticket_id: 30482, + ticket_url: "https://republic-widgets.zendesk.com/agent/tickets/30482", + subject: "Unable to reset my password", + description: "I tried the password reset link three times and never received the email.", + status: "new", + priority: "normal", + type: "question", + tags: ["password", "login"], + group: "Support Tier 1", + organization: "Acme Corp", + requester_name: "Jamie Chen", + requester_email: "jamie.chen@example.com", + assignee_name: "", + assignee_email: "", + created_at: "2026-07-10T14:02:31Z", + updated_at: "2026-07-10T14:02:31Z", + }, + docsUrl: "https://developer.zendesk.com/documentation/webhooks/verifying/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zendesk", + providerLabel: "Zendesk", + eventType: "ticket.updated", + name: "Ticket assigned", + description: "The ticket was picked up and assigned to an agent; status moved to open.", + body: { + ticket_id: 30482, + ticket_url: "https://republic-widgets.zendesk.com/agent/tickets/30482", + subject: "Unable to reset my password", + description: "I tried the password reset link three times and never received the email.", + status: "open", + priority: "normal", + type: "question", + tags: ["password", "login"], + group: "Support Tier 1", + organization: "Acme Corp", + requester_name: "Jamie Chen", + requester_email: "jamie.chen@example.com", + assignee_name: "Morgan Reyes", + assignee_email: "morgan.reyes@republic-widgets.example.com", + created_at: "2026-07-10T14:02:31Z", + updated_at: "2026-07-10T14:05:47Z", + }, + docsUrl: "https://developer.zendesk.com/documentation/webhooks/verifying/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zendesk", + providerLabel: "Zendesk", + eventType: "ticket.updated", + name: "Ticket priority escalated", + description: + "Priority was raised to urgent after the requester replied that billing was affected.", + body: { + ticket_id: 30510, + ticket_url: "https://republic-widgets.zendesk.com/agent/tickets/30510", + subject: "Duplicate charge on last invoice", + description: "My card was charged twice for the July invoice, please refund the duplicate.", + status: "open", + priority: "urgent", + type: "incident", + tags: ["billing", "refund"], + group: "Billing", + organization: "Acme Corp", + requester_name: "Dana Okafor", + requester_email: "dana.okafor@example.com", + assignee_name: "Priya Nair", + assignee_email: "priya.nair@republic-widgets.example.com", + created_at: "2026-07-11T09:15:02Z", + updated_at: "2026-07-11T09:41:18Z", + }, + docsUrl: "https://developer.zendesk.com/documentation/webhooks/verifying/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zendesk", + providerLabel: "Zendesk", + eventType: "ticket.updated", + name: "Public reply added", + description: "An agent posted a public comment; `latest_comment` carries the reply text.", + body: { + ticket_id: 30510, + ticket_url: "https://republic-widgets.zendesk.com/agent/tickets/30510", + subject: "Duplicate charge on last invoice", + status: "pending", + priority: "urgent", + type: "incident", + requester_name: "Dana Okafor", + requester_email: "dana.okafor@example.com", + assignee_name: "Priya Nair", + assignee_email: "priya.nair@republic-widgets.example.com", + latest_comment: + "Thanks for flagging this - I've refunded the duplicate charge, you should see it in 3-5 business days.", + latest_comment_is_public: true, + created_at: "2026-07-11T09:15:02Z", + updated_at: "2026-07-11T10:02:55Z", + }, + docsUrl: "https://developer.zendesk.com/documentation/webhooks/verifying/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zendesk", + providerLabel: "Zendesk", + eventType: "ticket.updated", + name: "Ticket solved", + description: "The ticket reached a terminal state after the requester confirmed the fix.", + body: { + ticket_id: 30482, + ticket_url: "https://republic-widgets.zendesk.com/agent/tickets/30482", + subject: "Unable to reset my password", + status: "solved", + priority: "normal", + type: "question", + tags: ["password", "login"], + group: "Support Tier 1", + organization: "Acme Corp", + requester_name: "Jamie Chen", + requester_email: "jamie.chen@example.com", + assignee_name: "Morgan Reyes", + assignee_email: "morgan.reyes@republic-widgets.example.com", + created_at: "2026-07-10T14:02:31Z", + updated_at: "2026-07-10T15:30:09Z", + }, + docsUrl: "https://developer.zendesk.com/documentation/webhooks/verifying/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/handAuthored/zoom.ts b/internal-packages/webhook-sources/src/handAuthored/zoom.ts new file mode 100644 index 00000000000..3e13798f868 --- /dev/null +++ b/internal-packages/webhook-sources/src/handAuthored/zoom.ts @@ -0,0 +1,194 @@ +import { type SampleRecord } from "../sampleRecord.js"; + +/** + * Zoom samples. Zoom signs webhooks with its own scheme (`x-zm-signature: v0=` over + * `v0:{x-zm-request-timestamp}:{rawBody}`, HMAC-SHA256), which does not match any of our verifier + * presets, so these ship sample-only (no `presetId`). Every event is a thin envelope + * (`event`, `event_ts`, `payload: { account_id, object }`) where `object` carries the meeting or + * recording resource. + */ +export const samples: SampleRecord[] = [ + { + provider: "zoom", + providerLabel: "Zoom", + eventType: "meeting.started", + name: "Meeting started", + description: "A scheduled meeting began. `payload.object.id` is the meeting id.", + body: { + event: "meeting.started", + event_ts: 1752562800000, + payload: { + account_id: "AAAAAABBBBBCCCCDDDD", + object: { + id: "84392028671", + uuid: "4nR0i/L9Q2mfrfXtEWMnGA==", + host_id: "z8yCxTTTTKWIFKy9d3VA", + topic: "Weekly Product Sync", + type: 8, + start_time: "2026-07-15T14:00:00Z", + timezone: "America/Los_Angeles", + duration: 60, + }, + }, + }, + docsUrl: "https://developers.zoom.us/docs/api/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zoom", + providerLabel: "Zoom", + eventType: "meeting.ended", + name: "Meeting ended", + description: "A meeting finished. `payload.object.end_time` marks when it closed.", + body: { + event: "meeting.ended", + event_ts: 1752566460000, + payload: { + account_id: "AAAAAABBBBBCCCCDDDD", + object: { + id: "84392028671", + uuid: "4nR0i/L9Q2mfrfXtEWMnGA==", + host_id: "z8yCxTTTTKWIFKy9d3VA", + topic: "Weekly Product Sync", + type: 8, + start_time: "2026-07-15T14:00:00Z", + end_time: "2026-07-15T15:01:00Z", + duration: 60, + }, + }, + }, + docsUrl: "https://developers.zoom.us/docs/api/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zoom", + providerLabel: "Zoom", + eventType: "meeting.participant_joined", + name: "Participant joined", + description: + "A participant joined the meeting. `payload.object.participant` carries their meeting-scoped id and join time.", + body: { + event: "meeting.participant_joined", + event_ts: 1752562860000, + payload: { + account_id: "AAAAAABBBBBCCCCDDDD", + object: { + id: "84392028671", + uuid: "4nR0i/L9Q2mfrfXtEWMnGA==", + host_id: "z8yCxTTTTKWIFKy9d3VA", + topic: "Weekly Product Sync", + type: 8, + start_time: "2026-07-15T14:00:00Z", + timezone: "America/Los_Angeles", + duration: 60, + participant: { + user_id: "16779264", + user_name: "Priya Nair", + id: "hK8fgZVOQkOKgOTgMFM2Jw", + join_time: "2026-07-15T14:01:00Z", + email: "priya.nair@example.com", + }, + }, + }, + }, + docsUrl: "https://developers.zoom.us/docs/api/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zoom", + providerLabel: "Zoom", + eventType: "meeting.participant_left", + name: "Participant left", + description: + "A participant left the meeting. `payload.object.participant.leave_reason` explains why (e.g. left voluntarily, disconnected).", + body: { + event: "meeting.participant_left", + event_ts: 1752565800000, + payload: { + account_id: "AAAAAABBBBBCCCCDDDD", + object: { + id: "84392028671", + uuid: "4nR0i/L9Q2mfrfXtEWMnGA==", + host_id: "z8yCxTTTTKWIFKy9d3VA", + topic: "Weekly Product Sync", + type: 8, + start_time: "2026-07-15T14:00:00Z", + timezone: "America/Los_Angeles", + duration: 60, + participant: { + user_id: "16779264", + user_name: "Priya Nair", + id: "hK8fgZVOQkOKgOTgMFM2Jw", + leave_time: "2026-07-15T14:50:00Z", + leave_reason: "left the meeting", + email: "priya.nair@example.com", + }, + }, + }, + }, + docsUrl: "https://developers.zoom.us/docs/api/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, + { + provider: "zoom", + providerLabel: "Zoom", + eventType: "recording.completed", + name: "Recording completed", + description: + "Cloud recording processing finished. `payload.object.recording_files` lists each file with its `download_url`; `payload.object.download_token` is a short-lived bearer token for fetching them.", + body: { + event: "recording.completed", + event_ts: 1752570120000, + payload: { + account_id: "AAAAAABBBBBCCCCDDDD", + object: { + id: "84392028671", + uuid: "4nR0i/L9Q2mfrfXtEWMnGA==", + account_id: "AAAAAABBBBBCCCCDDDD", + host_id: "z8yCxTTTTKWIFKy9d3VA", + topic: "Weekly Product Sync", + type: 8, + start_time: "2026-07-15T14:00:00Z", + timezone: "America/Los_Angeles", + host_email: "priya.nair@example.com", + duration: 60, + total_size: 52428800, + recording_count: 2, + share_url: "https://zoom.us/rec/share/abcDEF123ghiJKL456mnoPQR789stuVWX", + recording_files: [ + { + id: "9f8e7d6c-5b4a-3210-9f8e-7d6c5b4a3210", + meeting_id: "4nR0i/L9Q2mfrfXtEWMnGA==", + recording_start: "2026-07-15T14:00:05Z", + recording_end: "2026-07-15T15:00:52Z", + file_type: "MP4", + file_extension: "MP4", + file_size: 41943040, + play_url: "https://zoom.us/rec/play/abcDEF123ghiJKL456mnoPQR789stuVWX", + download_url: "https://zoom.us/rec/download/abcDEF123ghiJKL456mnoPQR789stuVWX", + status: "completed", + recording_type: "shared_screen_with_speaker_view", + }, + { + id: "1a2b3c4d-5e6f-7890-1a2b-3c4d5e6f7890", + meeting_id: "4nR0i/L9Q2mfrfXtEWMnGA==", + recording_start: "2026-07-15T14:00:05Z", + recording_end: "2026-07-15T15:00:52Z", + file_type: "M4A", + file_extension: "M4A", + file_size: 10485760, + play_url: "https://zoom.us/rec/play/xyz987UVW654rstQPO321nml", + download_url: "https://zoom.us/rec/download/xyz987UVW654rstQPO321nml", + status: "completed", + recording_type: "audio_only", + }, + ], + recording_play_passcode: "aB3dE9fG", + download_token: "eyJhbGciOiJIUzUxMiJ9.fake-sample-token-not-real.signature", + }, + }, + }, + docsUrl: "https://developers.zoom.us/docs/api/webhooks/", + provenance: { kind: "handauthored", snapshotDate: "2026-07" }, + }, +]; diff --git a/internal-packages/webhook-sources/src/index.ts b/internal-packages/webhook-sources/src/index.ts new file mode 100644 index 00000000000..77e3179565b --- /dev/null +++ b/internal-packages/webhook-sources/src/index.ts @@ -0,0 +1,25 @@ +import { type SampleManifestItem, type SampleRecord } from "./sampleRecord.js"; +import { samples } from "./samples.js"; + +export { SampleProvenance, SampleRecord } from "./sampleRecord.js"; +export type { SampleManifestItem } from "./sampleRecord.js"; +export { samples } from "./samples.js"; +export { registry, registryEntries, getProvider } from "./registry/index.js"; +export type { + ProviderRegistryEntry, + EventTypeSource, + SecretProvisioning, + SampleSource, +} from "./registry/types.js"; +export { WEBHOOK_CATEGORIES, categoryLabel, categoryOrder } from "./registry/categories.js"; +export type { WebhookCategoryId } from "./registry/categories.js"; + +/** Picker metadata (no bodies), small enough to ship to the client in one response. */ +export function sampleManifest(): SampleManifestItem[] { + return samples.map(({ body, extraHeaders, ...meta }) => meta); +} + +/** A single sample's full record (with body + headers), looked up on selection. */ +export function getSample(provider: string, eventType: string): SampleRecord | undefined { + return samples.find((sample) => sample.provider === provider && sample.eventType === eventType); +} diff --git a/internal-packages/webhook-sources/src/ingest.ts b/internal-packages/webhook-sources/src/ingest.ts new file mode 100644 index 00000000000..915c168ed5a --- /dev/null +++ b/internal-packages/webhook-sources/src/ingest.ts @@ -0,0 +1,115 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { type SampleRecord } from "./sampleRecord.js"; + +/** + * Snapshots the MIT-licensed Hookdeck webhook-samples dataset (samples.hookdeck.com) into a normalized + * set of SampleRecords committed to src/generated. Run manually to refresh: `pnpm run ingest`. + * Attribution lives in NOTICE.md. This is tooling, never imported at runtime. + */ + +const BASE = "https://samples.hookdeck.com"; +const SNAPSHOT_DATE = "2026-07"; +const PRESET_PROVIDERS = new Set(["stripe", "github", "svix", "square", "discord"]); + +const HEADER_DENYLIST = new Set([ + "content-type", + "content-length", + "host", + "accept", + "accept-encoding", + "accept-language", + "user-agent", + "connection", + "cache-control", + "pragma", + "origin", + "referer", + "date", +]); + +function isNoiseHeader(lower: string): boolean { + if (HEADER_DENYLIST.has(lower)) return true; + if (lower.startsWith("sec-") || lower.startsWith("x-forwarded") || lower.startsWith("cf-")) { + return true; + } + return /signature|hmac|authorization|-delivery|hook-id|hook-installation|request-id|webhook-id/.test( + lower + ); +} + +function cleanHeaders(headers: unknown): Record | undefined { + if (typeof headers !== "object" || headers === null) return undefined; + const out: Record = {}; + for (const [key, value] of Object.entries(headers as Record)) { + if (isNoiseHeader(key.toLowerCase())) continue; + if (typeof value === "string") out[key] = value; + } + return Object.keys(out).length > 0 ? out : undefined; +} + +async function fetchJson(path: string): Promise { + const res = await fetch(`${BASE}${path}`); + if (!res.ok) throw new Error(`${path} -> ${res.status}`); + return res.json(); +} + +type ProviderMeta = { label?: string; latest_version?: string }; + +async function main() { + const providers = (await fetchJson("/providers.json")) as Record; + const records: SampleRecord[] = []; + + for (const [provider, meta] of Object.entries(providers)) { + const version = meta.latest_version; + if (!version) continue; + let topics: Record; + try { + topics = (await fetchJson(`/providers/${provider}/${version}.json`)) as typeof topics; + } catch (error) { + console.warn(`skip ${provider}: ${String(error)}`); + continue; + } + + for (const [key, entry] of Object.entries(topics)) { + if (!entry || typeof entry !== "object" || entry.body === undefined) continue; + const eventType = entry.topic || key; + records.push({ + provider, + providerLabel: meta.label, + presetId: PRESET_PROVIDERS.has(provider) + ? (provider as SampleRecord["presetId"]) + : undefined, + eventType, + name: eventType, + body: entry.body, + extraHeaders: cleanHeaders(entry.headers), + provenance: { + kind: "upstream", + ref: `hookdeck/webhook-samples:${provider}/${version}`, + snapshotDate: SNAPSHOT_DATE, + }, + }); + } + } + + records.sort((a, b) => + a.provider === b.provider + ? a.eventType.localeCompare(b.eventType) + : a.provider.localeCompare(b.provider) + ); + + const here = dirname(fileURLToPath(import.meta.url)); + const outDir = join(here, "generated"); + mkdirSync(outDir, { recursive: true }); + writeFileSync(join(outDir, "hookdeck-samples.json"), JSON.stringify(records, null, 2) + "\n"); + + const providerCount = new Set(records.map((r) => r.provider)).size; + console.log(`wrote ${records.length} samples across ${providerCount} providers`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/internal-packages/webhook-sources/src/registry/adyen.ts b/internal-packages/webhook-sources/src/registry/adyen.ts new file mode 100644 index 00000000000..f8ff7865501 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/adyen.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "adyen", + label: "Adyen", + category: "payments", + docsUrl: "https://docs.adyen.com/development-resources/webhooks/", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "notificationItems.0.NotificationRequestItem.eventCode" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/anthropic.ts b/internal-packages/webhook-sources/src/registry/anthropic.ts new file mode 100644 index 00000000000..ca5999edaf3 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/anthropic.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Anthropic (Managed Agents) webhooks. Every delivery carries a single `X-Webhook-Signature` + * header over a `whsec_`-prefixed secret — not the three-header Standard Webhooks scheme + * (`webhook-id`/`webhook-timestamp`/`webhook-signature`) our `svix` preset verifies, so no + * preset is set here. The discriminant lives at `data.type`; the top-level `type` is always + * the literal string `"event"`. + */ +export const entry: ProviderRegistryEntry = { + id: "anthropic", + label: "Anthropic (Claude)", + category: "ai-platform", + docsUrl: "https://platform.claude.com/docs/en/managed-agents/webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "data.type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/assemblyai.ts b/internal-packages/webhook-sources/src/registry/assemblyai.ts new file mode 100644 index 00000000000..1be182ad023 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/assemblyai.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * AssemblyAI authenticates webhooks with a static shared secret: the integrator supplies + * `webhook_auth_header_name` / `webhook_auth_header_value` when creating the transcript, and + * AssemblyAI echoes that header back verbatim on delivery. There is no HMAC or Ed25519 signature, so + * no preset applies. The payload only ever carries `transcript_id` and `status` (`completed` or + * `error`) - callers must GET `/v2/transcript/{id}` separately for the transcript text or error detail. + */ +export const entry: ProviderRegistryEntry = { + id: "assemblyai", + label: "AssemblyAI", + category: "ai-platform", + docsUrl: "https://www.assemblyai.com/docs/concepts/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "status" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/attio.ts b/internal-packages/webhook-sources/src/registry/attio.ts new file mode 100644 index 00000000000..34fce170b51 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/attio.ts @@ -0,0 +1,17 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Attio signs with its own `attio-signature` header (legacy alias `X-Attio-Signature`): a bare hex + * HMAC-SHA256 digest of the raw body, no `sha256=` prefix, no timestamp component, no svix-style + * id/timestamp/signature triple. That doesn't match our `github` preset's `sha256=` format, our + * `stripe` preset's `t=,v1=` format, or `svix`'s header set, so this ships sample-only. + */ +export const entry: ProviderRegistryEntry = { + id: "attio", + label: "Attio", + category: "crm", + docsUrl: "https://docs.attio.com/rest-api/guides/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "events.0.event_type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/auth0.ts b/internal-packages/webhook-sources/src/registry/auth0.ts new file mode 100644 index 00000000000..153fb51407a --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/auth0.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Auth0 log streams authenticate with a static bearer token compared against the Authorization + * header, not an HMAC/Ed25519 signature, so no preset applies. Deliveries are a JSON array of log + * events (JSON Lines is also selectable, but array is the default Content Format); each element wraps + * the log event under `data`, so the event kind lives at `data.type` (e.g. "s" success login, "ss" + * success signup, "fp"/"fu" failed login variants). + */ +export const entry: ProviderRegistryEntry = { + id: "auth0", + label: "Auth0", + category: "auth-identity", + docsUrl: "https://auth0.com/docs/customize/log-streams/custom-log-streams", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "0.data.type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/bigcommerce.ts b/internal-packages/webhook-sources/src/registry/bigcommerce.ts new file mode 100644 index 00000000000..002cb01e034 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/bigcommerce.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "bigcommerce", + label: "BigCommerce", + category: "commerce", + docsUrl: "https://developer.bigcommerce.com/docs/integrations/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "scope" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/bitbucket.ts b/internal-packages/webhook-sources/src/registry/bitbucket.ts new file mode 100644 index 00000000000..9e42115b306 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/bitbucket.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "bitbucket", + label: "Bitbucket", + category: "source-control", + docsUrl: "https://support.atlassian.com/bitbucket-cloud/docs/manage-webhooks/", + secretProvisioning: "integrator", + eventTypeSource: { from: "header", name: "x-event-key" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/brex.ts b/internal-packages/webhook-sources/src/registry/brex.ts new file mode 100644 index 00000000000..088f34464b0 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/brex.ts @@ -0,0 +1,19 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Brex signs webhooks with the Standard Webhooks scheme: `Webhook-Id` / `Webhook-Timestamp` / + * `Webhook-Signature` headers, HMAC-SHA256 base64 over `{id}.{timestamp}.{rawBody}`, verified + * against a base64-decoded secret from `GET /v1/webhooks/secrets` (their docs' own example secret + * has no `whsec_` prefix, but our `svix` preset's prefix strip is a no-op when absent). Matches + * our `svix` preset. The discriminant is `event_type` (not `type`) at the top level of the body. + */ +export const entry: ProviderRegistryEntry = { + id: "brex", + label: "Brex", + category: "fintech", + docsUrl: "https://developer.brex.com/docs/webhooks/", + preset: "svix", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event_type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/cal-com.ts b/internal-packages/webhook-sources/src/registry/cal-com.ts new file mode 100644 index 00000000000..ad5b90f0ed7 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/cal-com.ts @@ -0,0 +1,16 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Cal.com signs webhooks with an `x-cal-signature-256` header, a plain hex HMAC-SHA256 digest of + * the raw JSON body (no `sha256=` prefix), so it does not match our `github` preset's wire format. + * Ships sample-only. The discriminant is `triggerEvent` at the top level of the body. + */ +export const entry: ProviderRegistryEntry = { + id: "cal-com", + label: "Cal.com", + category: "calendar-scheduling", + docsUrl: "https://cal.com/docs/developing/guides/automation/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "triggerEvent" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/calendly.ts b/internal-packages/webhook-sources/src/registry/calendly.ts new file mode 100644 index 00000000000..cec0b8189ca --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/calendly.ts @@ -0,0 +1,16 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Calendly signs with its own `Calendly-Webhook-Signature` header (`t=,v1=`, HMAC-SHA256 over + * `{t}.{rawBody}`), not `stripe-signature`. The scheme matches our stripe preset's math but not its wire + * format (header name), so no preset is set here; ships sample-only. + */ +export const entry: ProviderRegistryEntry = { + id: "calendly", + label: "Calendly", + category: "calendar-scheduling", + docsUrl: "https://developer.calendly.com/api-docs/4c305798a61d3-webhook-signatures", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/categories.ts b/internal-packages/webhook-sources/src/registry/categories.ts new file mode 100644 index 00000000000..f6cb0988d4b --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/categories.ts @@ -0,0 +1,45 @@ +/** + * Canonical provider categories: the single source of truth for a category's display label and the + * order categories appear in (the console groups the picker by these). A registry entry's `category` + * must be one of these ids; the webapp renders `categoryLabel(id)` rather than deriving anything. + */ +export const WEBHOOK_CATEGORIES = [ + { id: "ai-platform", label: "AI & ML" }, + { id: "voice", label: "Voice AI" }, + { id: "communication", label: "Communication" }, + { id: "email", label: "Email" }, + { id: "marketing", label: "Marketing" }, + { id: "support", label: "Support" }, + { id: "crm", label: "CRM" }, + { id: "pm", label: "Project Management" }, + { id: "productivity", label: "Productivity" }, + { id: "calendar-scheduling", label: "Calendar & Scheduling" }, + { id: "forms", label: "Forms" }, + { id: "e-signature", label: "E-Signature" }, + { id: "storage", label: "Storage" }, + { id: "data", label: "Data" }, + { id: "source-control", label: "Source Control" }, + { id: "ci-cd", label: "CI / CD" }, + { id: "hosting-infra", label: "Hosting & Infra" }, + { id: "observability", label: "Observability" }, + { id: "auth-identity", label: "Auth & Identity" }, + { id: "payments", label: "Payments" }, + { id: "billing", label: "Billing & Subscriptions" }, + { id: "commerce", label: "Commerce" }, + { id: "fintech", label: "Fintech & Banking" }, +] as const; + +export type WebhookCategoryId = (typeof WEBHOOK_CATEGORIES)[number]["id"]; + +const LABEL = new Map(WEBHOOK_CATEGORIES.map((c) => [c.id, c.label])); +const ORDER = new Map(WEBHOOK_CATEGORIES.map((c, i) => [c.id, i])); + +/** Display label for a category id, falling back to the id itself for anything unmapped. */ +export function categoryLabel(id: string): string { + return LABEL.get(id) ?? id; +} + +/** Sort index for a category id (unmapped categories sort last). */ +export function categoryOrder(id: string): number { + return ORDER.get(id) ?? Number.MAX_SAFE_INTEGER; +} diff --git a/internal-packages/webhook-sources/src/registry/checkout.ts b/internal-packages/webhook-sources/src/registry/checkout.ts new file mode 100644 index 00000000000..6dcc7965026 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/checkout.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "checkout", + label: "Checkout.com", + category: "payments", + docsUrl: "https://www.checkout.com/docs/developer-resources/event-notifications/receive-webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/clerk.ts b/internal-packages/webhook-sources/src/registry/clerk.ts new file mode 100644 index 00000000000..1ad9bc721e4 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/clerk.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "clerk", + label: "Clerk", + category: "auth-identity", + docsUrl: "https://clerk.com/docs/integrations/webhooks/overview", + preset: "svix", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/close-crm.ts b/internal-packages/webhook-sources/src/registry/close-crm.ts new file mode 100644 index 00000000000..7492ad4889d --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/close-crm.ts @@ -0,0 +1,21 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Close signs webhooks with a custom scheme: the `close-sig-hash` header carries a hex-encoded + * HMAC-SHA256 signature computed over `{close-sig-timestamp}{rawBody}` (timestamp in a sibling + * header, key hex-decoded before use). That doesn't match `stripe` (`t=,v1=` combined into one + * header value), `square` (base64 digest), `github` (no timestamp component), `svix` (three + * id/timestamp/signature headers, base64), or `discord` (Ed25519), so this ships sample-only with + * no preset. The resource type rides the body's nested `event.object_type` plus `event.action` + * (e.g. `lead` + `created`, `activity.note` + `created`); `event.action` is the primary + * discriminant field. + */ +export const entry: ProviderRegistryEntry = { + id: "close-crm", + label: "Close", + category: "crm", + docsUrl: "https://developer.close.com/topics/webhooks/", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event.action" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/commercelayer.ts b/internal-packages/webhook-sources/src/registry/commercelayer.ts new file mode 100644 index 00000000000..ed217908821 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/commercelayer.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "commercelayer", + label: "Commerce Layer", + category: "commerce", + docsUrl: "https://docs.commercelayer.io/core/callbacks-security", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "data.type" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/deepgram.ts b/internal-packages/webhook-sources/src/registry/deepgram.ts new file mode 100644 index 00000000000..c187b9d195d --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/deepgram.ts @@ -0,0 +1,22 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Deepgram's async transcription callback carries a `dg-token` header set to the API Key Identifier + * that submitted the original request - an identifier, not an HMAC/Ed25519 signature - so no preset + * applies. Protecting the endpoint is left to the integrator (embed Basic Auth credentials in the + * callback URL itself, or IP-allowlist Deepgram's senders); Deepgram never issues a per-endpoint secret. + * The callback body is the raw transcription result object (metadata + results), with no discriminant + * field at all. `results.channels` is present only on success; a failed job instead delivers + * `{ err_code, err_msg, request_id }` (Deepgram's general API error shape). `eventTypeSource` points at + * `results.channels` as the representative success case; each sample's explicit `eventType` is + * authoritative. + */ +export const entry: ProviderRegistryEntry = { + id: "deepgram", + label: "Deepgram", + category: "ai-platform", + docsUrl: "https://developers.deepgram.com/docs/callback", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "results.channels" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/discord.ts b/internal-packages/webhook-sources/src/registry/discord.ts new file mode 100644 index 00000000000..49fe26bf0d7 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/discord.ts @@ -0,0 +1,19 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Discord Webhook Events. Verified against docs: two headers (`X-Signature-Ed25519` + + * `X-Signature-Timestamp`), Ed25519 over `{timestamp}{rawBody}` - exact match for our `discord` + * preset. The outer `type` field is only a wrapper (`0` = PING, `1` = event) and is NOT a useful + * discriminant; the real event kind lives at `event.type` (e.g. `APPLICATION_AUTHORIZED`), so + * `eventTypeSource` points there instead of the literal top-level `type` path. + */ +export const entry: ProviderRegistryEntry = { + id: "discord", + label: "Discord", + category: "communication", + docsUrl: "https://docs.discord.com/developers/events/webhook-events", + preset: "discord", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event.type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/docusign.ts b/internal-packages/webhook-sources/src/registry/docusign.ts new file mode 100644 index 00000000000..115d486e5d4 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/docusign.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "docusign", + label: "DocuSign", + category: "e-signature", + docsUrl: "https://developers.docusign.com/platform/webhooks/connect/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/elevenlabs.ts b/internal-packages/webhook-sources/src/registry/elevenlabs.ts new file mode 100644 index 00000000000..f0f9d8afa54 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/elevenlabs.ts @@ -0,0 +1,17 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * ElevenLabs signs post-call webhooks with `elevenlabs-signature: t=,v0=`, an HMAC-SHA256 + * of `{timestamp}.{rawBody}`. Structurally close to our `stripe` preset (timestamped HMAC-SHA256), but + * the header name and the `v0=` value prefix (vs. `v1=`) don't match exactly, so this ships sample-only + * (no preset) until a custom verifier config exists for this wire format. + */ +export const entry: ProviderRegistryEntry = { + id: "elevenlabs", + label: "ElevenLabs", + category: "voice", + docsUrl: "https://elevenlabs.io/docs/eleven-agents/workflows/post-call-webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/github.ts b/internal-packages/webhook-sources/src/registry/github.ts new file mode 100644 index 00000000000..95fb02b8c33 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/github.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "github", + label: "GitHub", + category: "source-control", + docsUrl: "https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries", + preset: "github", + secretProvisioning: "integrator", + eventTypeSource: { from: "header", name: "x-github-event" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/gitlab.ts b/internal-packages/webhook-sources/src/registry/gitlab.ts new file mode 100644 index 00000000000..820ca59ee93 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/gitlab.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "gitlab", + label: "GitLab", + category: "source-control", + docsUrl: "https://docs.gitlab.com/user/project/integrations/webhooks/", + preset: "svix", + secretProvisioning: "integrator", + eventTypeSource: { from: "header", name: "x-gitlab-event" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/hubspot.ts b/internal-packages/webhook-sources/src/registry/hubspot.ts new file mode 100644 index 00000000000..ca43d19f137 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/hubspot.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "hubspot", + label: "HubSpot", + category: "crm", + docsUrl: "https://developers.hubspot.com/docs/guides/api/app-management/webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "0.subscriptionType" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/index.ts b/internal-packages/webhook-sources/src/registry/index.ts new file mode 100644 index 00000000000..dca38dce75c --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/index.ts @@ -0,0 +1,123 @@ +/** GENERATED by catalog/build-aggregates.ts. Do not edit by hand. */ +import { type ProviderRegistryEntry } from "./types.js"; +import { entry as e_adyen } from "./adyen.js"; +import { entry as e_anthropic } from "./anthropic.js"; +import { entry as e_assemblyai } from "./assemblyai.js"; +import { entry as e_attio } from "./attio.js"; +import { entry as e_auth0 } from "./auth0.js"; +import { entry as e_bigcommerce } from "./bigcommerce.js"; +import { entry as e_bitbucket } from "./bitbucket.js"; +import { entry as e_brex } from "./brex.js"; +import { entry as e_cal_com } from "./cal-com.js"; +import { entry as e_calendly } from "./calendly.js"; +import { entry as e_checkout } from "./checkout.js"; +import { entry as e_clerk } from "./clerk.js"; +import { entry as e_close_crm } from "./close-crm.js"; +import { entry as e_commercelayer } from "./commercelayer.js"; +import { entry as e_deepgram } from "./deepgram.js"; +import { entry as e_discord } from "./discord.js"; +import { entry as e_docusign } from "./docusign.js"; +import { entry as e_elevenlabs } from "./elevenlabs.js"; +import { entry as e_github } from "./github.js"; +import { entry as e_gitlab } from "./gitlab.js"; +import { entry as e_hubspot } from "./hubspot.js"; +import { entry as e_intercom } from "./intercom.js"; +import { entry as e_jira } from "./jira.js"; +import { entry as e_linear } from "./linear.js"; +import { entry as e_monday } from "./monday.js"; +import { entry as e_notion } from "./notion.js"; +import { entry as e_openai } from "./openai.js"; +import { entry as e_paddlebilling } from "./paddlebilling.js"; +import { entry as e_paddleclassic } from "./paddleclassic.js"; +import { entry as e_pagerduty } from "./pagerduty.js"; +import { entry as e_paypal } from "./paypal.js"; +import { entry as e_pipedrive } from "./pipedrive.js"; +import { entry as e_plaid } from "./plaid.js"; +import { entry as e_postmark } from "./postmark.js"; +import { entry as e_recall_ai } from "./recall-ai.js"; +import { entry as e_replicate } from "./replicate.js"; +import { entry as e_resend } from "./resend.js"; +import { entry as e_retell } from "./retell.js"; +import { entry as e_sendgrid } from "./sendgrid.js"; +import { entry as e_sentry } from "./sentry.js"; +import { entry as e_shopify } from "./shopify.js"; +import { entry as e_slack } from "./slack.js"; +import { entry as e_square } from "./square.js"; +import { entry as e_stripe } from "./stripe.js"; +import { entry as e_supabase } from "./supabase.js"; +import { entry as e_telegram } from "./telegram.js"; +import { entry as e_twilio } from "./twilio.js"; +import { entry as e_typeform } from "./typeform.js"; +import { entry as e_vapi } from "./vapi.js"; +import { entry as e_vercel } from "./vercel.js"; +import { entry as e_whatsapp } from "./whatsapp.js"; +import { entry as e_woocommerce } from "./woocommerce.js"; +import { entry as e_workos } from "./workos.js"; +import { entry as e_zendesk } from "./zendesk.js"; +import { entry as e_zoom } from "./zoom.js"; + +export const registryEntries: ProviderRegistryEntry[] = [ + e_adyen, + e_anthropic, + e_assemblyai, + e_attio, + e_auth0, + e_bigcommerce, + e_bitbucket, + e_brex, + e_cal_com, + e_calendly, + e_checkout, + e_clerk, + e_close_crm, + e_commercelayer, + e_deepgram, + e_discord, + e_docusign, + e_elevenlabs, + e_github, + e_gitlab, + e_hubspot, + e_intercom, + e_jira, + e_linear, + e_monday, + e_notion, + e_openai, + e_paddlebilling, + e_paddleclassic, + e_pagerduty, + e_paypal, + e_pipedrive, + e_plaid, + e_postmark, + e_recall_ai, + e_replicate, + e_resend, + e_retell, + e_sendgrid, + e_sentry, + e_shopify, + e_slack, + e_square, + e_stripe, + e_supabase, + e_telegram, + e_twilio, + e_typeform, + e_vapi, + e_vercel, + e_whatsapp, + e_woocommerce, + e_workos, + e_zendesk, + e_zoom, +]; + +export const registry: Record = Object.fromEntries( + registryEntries.map((e) => [e.id, e]) +); + +export function getProvider(id: string): ProviderRegistryEntry | undefined { + return registry[id]; +} diff --git a/internal-packages/webhook-sources/src/registry/intercom.ts b/internal-packages/webhook-sources/src/registry/intercom.ts new file mode 100644 index 00000000000..d03a1a98d2e --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/intercom.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Intercom signs webhook notifications with an HMAC-SHA1 digest in the `X-Hub-Signature` header + * (over the raw body, keyed by the app's client secret) - that is SHA1, not the SHA256 our github + * preset requires, so this ships sample-only with no preset. Every notification is wrapped in the + * same envelope (`type: "notification_event"`, `topic`, `data.item`), with `topic` carrying the + * event discriminant. + */ +export const entry: ProviderRegistryEntry = { + id: "intercom", + label: "Intercom", + category: "support", + docsUrl: "https://developers.intercom.com/docs/references/webhooks/webhook-models", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "topic" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/jira.ts b/internal-packages/webhook-sources/src/registry/jira.ts new file mode 100644 index 00000000000..59ca16dd3fa --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/jira.ts @@ -0,0 +1,16 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Jira Cloud signs webhooks with an `X-Hub-Signature` header (no `-256` suffix, value `sha256=` + * per the WebSub convention), which does not match our `github` preset's `x-hub-signature-256` header + * name. Shipping sample-only until a custom verifier config exists for this wire format. + */ +export const entry: ProviderRegistryEntry = { + id: "jira", + label: "Jira", + category: "pm", + docsUrl: "https://developer.atlassian.com/cloud/jira/platform/webhooks/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "webhookEvent" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/linear.ts b/internal-packages/webhook-sources/src/registry/linear.ts new file mode 100644 index 00000000000..e55f9006c25 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/linear.ts @@ -0,0 +1,19 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Linear signs webhooks with a custom scheme: a `Linear-Signature` header carrying a hex-encoded + * HMAC-SHA256 signature of the raw body, keyed by the webhook's signing secret. The header name and + * value format don't match our `github` preset (`x-hub-signature-256`, `sha256=` prefix) or any + * other preset, so this ships sample-only with no preset. The resource type rides the top-level `type` + * field in the body, but the meaningful event also depends on the top-level `action` (create/update/ + * remove). + */ +export const entry: ProviderRegistryEntry = { + id: "linear", + label: "Linear", + category: "pm", + docsUrl: "https://linear.app/developers/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/monday.ts b/internal-packages/webhook-sources/src/registry/monday.ts new file mode 100644 index 00000000000..fd5580e8685 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/monday.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "monday", + label: "monday.com", + category: "pm", + docsUrl: "https://developer.monday.com/api-reference/docs/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event.type" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/notion.ts b/internal-packages/webhook-sources/src/registry/notion.ts new file mode 100644 index 00000000000..c1ca8e0fd89 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/notion.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "notion", + label: "Notion", + category: "productivity", + docsUrl: "https://developers.notion.com/reference/webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/openai.ts b/internal-packages/webhook-sources/src/registry/openai.ts new file mode 100644 index 00000000000..0dd312f0572 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/openai.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "openai", + label: "OpenAI", + category: "ai-platform", + docsUrl: "https://platform.openai.com/docs/guides/webhooks", + preset: "svix", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/paddlebilling.ts b/internal-packages/webhook-sources/src/registry/paddlebilling.ts new file mode 100644 index 00000000000..d93e158868e --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/paddlebilling.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "paddlebilling", + label: "Paddle Billing", + category: "billing", + docsUrl: "https://developer.paddle.com/webhooks/overview", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event_type" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/paddleclassic.ts b/internal-packages/webhook-sources/src/registry/paddleclassic.ts new file mode 100644 index 00000000000..da167611523 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/paddleclassic.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "paddleclassic", + label: "Paddle Classic", + category: "billing", + docsUrl: "https://developer.paddle.com/classic/reference/ZG9jOjI1MzUzOTg2-verifying-webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "alert_name" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/pagerduty.ts b/internal-packages/webhook-sources/src/registry/pagerduty.ts new file mode 100644 index 00000000000..e1d1bc54a85 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/pagerduty.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * PagerDuty v3 webhook subscriptions sign with `X-PagerDuty-Signature: v1=`, an HMAC-SHA256 of + * the raw request body only (no timestamp component in the signed string), which does not match any + * of our presets (stripe needs a `t=` timestamp folded into the signing input, github's header name + * and svix/square/discord's schemes are all different). Shipping sample-only until a custom verifier + * config exists for this wire format. + */ +export const entry: ProviderRegistryEntry = { + id: "pagerduty", + label: "PagerDuty", + category: "observability", + docsUrl: "https://developer.pagerduty.com/docs/webhooks/webhook-signatures/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event.event_type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/paypal.ts b/internal-packages/webhook-sources/src/registry/paypal.ts new file mode 100644 index 00000000000..8daaf1c6656 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/paypal.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "paypal", + label: "PayPal", + category: "payments", + docsUrl: "https://developer.paypal.com/api/rest/webhooks/rest/", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event_type" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/pipedrive.ts b/internal-packages/webhook-sources/src/registry/pipedrive.ts new file mode 100644 index 00000000000..5e28b2c3f81 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/pipedrive.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "pipedrive", + label: "Pipedrive", + category: "crm", + docsUrl: "https://developers.pipedrive.com/docs/api/v1/Webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/plaid.ts b/internal-packages/webhook-sources/src/registry/plaid.ts new file mode 100644 index 00000000000..1b93f2b381a --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/plaid.ts @@ -0,0 +1,21 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Plaid signs webhooks with a `Plaid-Verification` header carrying a JWT (ES256: ECDSA over + * SHA-256), whose payload holds `iat` and a `request_body_sha256` hash of the raw body; the + * receiver fetches the signing JWK from `/webhook_verification_key/get` by the JWT's `kid`. This + * is a bespoke JWS + separate-key-fetch scheme, not a shared-secret HMAC, so it does not match any + * of stripe/github/svix/square/discord - ships sample-only with no preset. `secretProvisioning` is + * "provider" because Plaid alone holds the signing key; there is no integrator-set secret. The + * discriminant is the top-level `webhook_type` (TRANSACTIONS, ITEM, ...); the finer-grained event + * also depends on `webhook_code` (and, for ITEM ERROR, the nested `error.error_code`). + */ +export const entry: ProviderRegistryEntry = { + id: "plaid", + label: "Plaid", + category: "fintech", + docsUrl: "https://plaid.com/docs/api/webhooks/webhook-verification/", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "webhook_type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/postmark.ts b/internal-packages/webhook-sources/src/registry/postmark.ts new file mode 100644 index 00000000000..7aa39eb21da --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/postmark.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Postmark does not support HMAC webhook signatures (confirmed on the webhooks overview page). Requests + * are secured with HTTP Basic Auth embedded in the webhook URL (`https://:@example.com/hook`) + * and/or IP allowlisting, so no verifier preset applies and this ships sample-only. The Delivery/Bounce/ + * SpamComplaint/Open event webhooks carry a `RecordType` field; the separate Inbound webhook (a distinct, + * older feature) does not - see the handAuthored file for how that sample is handled. + */ +export const entry: ProviderRegistryEntry = { + id: "postmark", + label: "Postmark", + category: "email", + docsUrl: "https://postmarkapp.com/developer/webhooks/inbound-webhook", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "RecordType" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/recall-ai.ts b/internal-packages/webhook-sources/src/registry/recall-ai.ts new file mode 100644 index 00000000000..71bfa4f943e --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/recall-ai.ts @@ -0,0 +1,16 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Recall.ai signs webhooks with Svix: Webhook-Id / Webhook-Timestamp / Webhook-Signature headers, + * HMAC-SHA256 base64 over "{id}.{timestamp}.{body}", secret prefixed "whsec_". Matches our svix preset. + */ +export const entry: ProviderRegistryEntry = { + id: "recall-ai", + label: "Recall.ai", + category: "ai-platform", + docsUrl: "https://docs.recall.ai/docs/authenticating-requests-from-recallai", + preset: "svix", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/replicate.ts b/internal-packages/webhook-sources/src/registry/replicate.ts new file mode 100644 index 00000000000..494a41db558 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/replicate.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "replicate", + label: "Replicate", + category: "ai-platform", + docsUrl: "https://replicate.com/docs/topics/webhooks/verify-webhook", + preset: "svix", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "status" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/resend.ts b/internal-packages/webhook-sources/src/registry/resend.ts new file mode 100644 index 00000000000..0c250bb86b4 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/resend.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "resend", + label: "Resend", + category: "email", + docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction", + preset: "svix", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/retell.ts b/internal-packages/webhook-sources/src/registry/retell.ts new file mode 100644 index 00000000000..c2af616a9ea --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/retell.ts @@ -0,0 +1,16 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Retell AI signs webhooks with its own scheme: `x-retell-signature: v={timestamp_ms},d={hex_digest}`, + * HMAC-SHA256 over `rawBody + timestamp` (string concatenation, not dot-joined), keyed by an API key + * (no `whsec_`-style prefix). This does not match any of our verifier presets, so these ship sample-only. + */ +export const entry: ProviderRegistryEntry = { + id: "retell", + label: "Retell AI", + category: "voice", + docsUrl: "https://docs.retellai.com/features/secure-webhook", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/sendgrid.ts b/internal-packages/webhook-sources/src/registry/sendgrid.ts new file mode 100644 index 00000000000..53ab09aecaf --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/sendgrid.ts @@ -0,0 +1,20 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * SendGrid's Event Webhook is signed with ECDSA (a private/public EC key pair SendGrid generates): + * `X-Twilio-Email-Event-Webhook-Signature` (base64 signature) and + * `X-Twilio-Email-Event-Webhook-Timestamp` over `{timestamp}{rawBody}`. That scheme is not among our + * stripe/github/svix/square/discord verifier presets, so this ships sample-only (no `preset`). SendGrid + * also delivers events in a BATCH: the POST body is a JSON array of event objects, so the discriminant + * lives at `0.event` (the first event in the array) rather than a top-level field. + */ +export const entry: ProviderRegistryEntry = { + id: "sendgrid", + label: "SendGrid", + category: "email", + docsUrl: + "https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "0.event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/sentry.ts b/internal-packages/webhook-sources/src/registry/sentry.ts new file mode 100644 index 00000000000..8910f9155eb --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/sentry.ts @@ -0,0 +1,17 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Sentry signs Integration Platform webhooks with a custom HMAC-SHA256 scheme (Sentry-Hook-Signature + * over the raw body, keyed by the integration's client secret) - it does not match stripe/github/svix/ + * square/discord, so this ships sample-only with no preset. The resource type (issue/error/comment/...) + * rides the `Sentry-Hook-Resource` header rather than the body. + */ +export const entry: ProviderRegistryEntry = { + id: "sentry", + label: "Sentry", + category: "observability", + docsUrl: "https://docs.sentry.io/product/integrations/integration-platform/webhooks/", + secretProvisioning: "integrator", + eventTypeSource: { from: "header", name: "sentry-hook-resource" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/shopify.ts b/internal-packages/webhook-sources/src/registry/shopify.ts new file mode 100644 index 00000000000..dc743cab9e9 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/shopify.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "shopify", + label: "Shopify", + category: "commerce", + docsUrl: "https://shopify.dev/docs/apps/build/webhooks/verify-deliveries", + secretProvisioning: "integrator", + eventTypeSource: { from: "header", name: "x-shopify-topic" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/slack.ts b/internal-packages/webhook-sources/src/registry/slack.ts new file mode 100644 index 00000000000..adeb10e9559 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/slack.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "slack", + label: "Slack", + category: "communication", + docsUrl: "https://docs.slack.dev/authentication/verifying-requests-from-slack/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event.type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/square.ts b/internal-packages/webhook-sources/src/registry/square.ts new file mode 100644 index 00000000000..2294f29b78a --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/square.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "square", + label: "Square", + category: "payments", + docsUrl: "https://developer.squareup.com/docs/webhooks/step3validate", + preset: "square", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/stripe.ts b/internal-packages/webhook-sources/src/registry/stripe.ts new file mode 100644 index 00000000000..3b739c489e7 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/stripe.ts @@ -0,0 +1,12 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "stripe", + label: "Stripe", + category: "payments", + docsUrl: "https://docs.stripe.com/webhooks", + preset: "stripe", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/supabase.ts b/internal-packages/webhook-sources/src/registry/supabase.ts new file mode 100644 index 00000000000..8854bacb3df --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/supabase.ts @@ -0,0 +1,19 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Supabase Database Webhooks have no built-in signing scheme - the docs describe only the payload + * shape (`type`/`table`/`schema`/`record`/`old_record`), and any authentication is whatever custom + * HTTP header the integrator adds to the underlying `net.http_post` call (e.g. a bearer token or a + * shared-secret header they check for themselves). That doesn't match any of our verifier presets, so + * this ships sample-only with no preset. The event discriminant is the top-level `type` field, one of + * `INSERT` / `UPDATE` / `DELETE`. + */ +export const entry: ProviderRegistryEntry = { + id: "supabase", + label: "Supabase", + category: "hosting-infra", + docsUrl: "https://supabase.com/docs/guides/database/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/telegram.ts b/internal-packages/webhook-sources/src/registry/telegram.ts new file mode 100644 index 00000000000..e81af84a29b --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/telegram.ts @@ -0,0 +1,19 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Telegram authenticates webhooks with a static shared secret compared against the + * X-Telegram-Bot-Api-Secret-Token header (set via the `secret_token` param to `setWebhook`), not an + * HMAC/Ed25519 signature, so no preset applies. The Update object has no single `type` field: the + * event kind is whichever top-level key is present (message, edited_message, callback_query, + * inline_query, ...). `eventTypeSource.path` names the most common case as a representative default; + * a real consumer needs to check for whichever of the known Update keys exists. + */ +export const entry: ProviderRegistryEntry = { + id: "telegram", + label: "Telegram", + category: "communication", + docsUrl: "https://core.telegram.org/bots/api#setwebhook", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "message" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/twilio.ts b/internal-packages/webhook-sources/src/registry/twilio.ts new file mode 100644 index 00000000000..3dcad23cf98 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/twilio.ts @@ -0,0 +1,21 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Twilio signs webhooks with `X-Twilio-Signature`: HMAC-SHA1, base64-encoded, over the exact webhook + * URL with sorted POST param names+values appended (JSON bodies instead append a `bodySHA256` query + * param). No preset matches (SHA-1, not SHA-256; URL+params, not a raw-body or `t.body` string), so this + * ships sample-only. Bodies are `application/x-www-form-urlencoded`, not JSON; every sample is the + * decoded key/value form object. Twilio has no single "type" field across products: inbound SMS/MMS + * carries a constant `SmsStatus` ("received"), while voice call requests carry `CallStatus`, which does + * vary (ringing/completed/busy/no-answer/...). `eventTypeSource` points at `CallStatus` as the closest + * real, varying discriminant; each sample's explicit `eventType` is the authoritative label. + */ +export const entry: ProviderRegistryEntry = { + id: "twilio", + label: "Twilio", + category: "communication", + docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "CallStatus" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/typeform.ts b/internal-packages/webhook-sources/src/registry/typeform.ts new file mode 100644 index 00000000000..8982483a9cc --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/typeform.ts @@ -0,0 +1,17 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Typeform signs webhooks with a `Typeform-Signature` header formatted as `sha256=` HMAC-SHA256 + * over the raw request body. The header name and base64 encoding don't match our `github` preset (which + * requires `x-hub-signature-256` with a hex-encoded digest), nor any other preset, so this ships + * sample-only without a custom verifier config. + */ +export const entry: ProviderRegistryEntry = { + id: "typeform", + label: "Typeform", + category: "forms", + docsUrl: "https://www.typeform.com/developers/webhooks/secure-your-webhooks/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event_type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/types.ts b/internal-packages/webhook-sources/src/registry/types.ts new file mode 100644 index 00000000000..59c9c559a2a --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/types.ts @@ -0,0 +1,32 @@ +import { type WebhookPresetId } from "@trigger.dev/core/webhooks"; +import { type WebhookCategoryId } from "./categories.js"; + +/** + * Where a provider's event "type" lives. Drives Hookdeck topic parsing and the console picker grouping. + * Body-discriminated providers keep the type in the payload (e.g. Stripe `type`); header-discriminated + * ones carry it in a header (e.g. GitHub `x-github-event`). + */ +export type EventTypeSource = { from: "body"; path: string } | { from: "header"; name: string }; + +/** Who holds the signing secret: the provider issues it, the integrator sets it, or either. */ +export type SecretProvisioning = "provider" | "integrator" | "either"; + +/** Where a provider's samples come from (see the sourcing pipeline in the plan). */ +export type SampleSource = "hookdeck" | "octokit" | "capture" | "handauthored"; + +/** + * One registry entry per provider. Adding a provider is one of these plus a source. `preset` is set + * only when the provider maps to one of our verifier presets (unlocks the round-trip guarantee); a + * provider without a preset ships sample-only until a custom verifier config is authored. + */ +export type ProviderRegistryEntry = { + id: string; + label: string; + category: WebhookCategoryId; + icon?: string; + docsUrl?: string; + preset?: WebhookPresetId; + secretProvisioning: SecretProvisioning; + eventTypeSource: EventTypeSource; + sampleSource: SampleSource; +}; diff --git a/internal-packages/webhook-sources/src/registry/vapi.ts b/internal-packages/webhook-sources/src/registry/vapi.ts new file mode 100644 index 00000000000..b2f8c8f688a --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/vapi.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Vapi signs server webhooks with a bespoke scheme: an `x-vapi-signature` header holding a raw + * HMAC-SHA256 hex digest of the raw body, keyed by the integrator-configured server secret. There is + * no timestamp component and no `sha256=`/base64 wrapping, so it does not match our + * stripe/github/svix/square/discord presets - this ships sample-only. Every server message is wrapped + * in a top-level `message` object whose `type` field is the event discriminant. + */ +export const entry: ProviderRegistryEntry = { + id: "vapi", + label: "Vapi", + category: "voice", + docsUrl: "https://docs.vapi.ai/server-url/server-authentication", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "message.type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/vercel.ts b/internal-packages/webhook-sources/src/registry/vercel.ts new file mode 100644 index 00000000000..945f622c813 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/vercel.ts @@ -0,0 +1,18 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Vercel signs webhooks with a bespoke scheme: the `x-vercel-signature` header carries a hex-encoded + * HMAC-SHA1 digest of the raw body (no `sha1=` prefix, no timestamp component). That doesn't match any + * of our presets (stripe/github/svix/square/discord all use SHA256 or Ed25519, and github's closest + * cousin still requires the `sha256=`-prefixed hex plus a `-256` header suffix), so this ships + * sample-only with no preset. The event name rides the top-level `type` field in the body. + */ +export const entry: ProviderRegistryEntry = { + id: "vercel", + label: "Vercel", + category: "hosting-infra", + docsUrl: "https://vercel.com/docs/webhooks", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "type" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/whatsapp.ts b/internal-packages/webhook-sources/src/registry/whatsapp.ts new file mode 100644 index 00000000000..deb6cac3bf0 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/whatsapp.ts @@ -0,0 +1,20 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * WhatsApp Cloud API (Meta). Signs with `X-Hub-Signature-256: sha256=` HMAC-SHA256 over the raw + * body, matching our `github` preset exactly. Note: Meta's real payloads always carry + * `entry[].changes[].field === "messages"` for both inbound messages AND status updates, so that + * field cannot discriminate the two; the real discriminant is which key (`value.messages` vs + * `value.statuses`) is present in the body. `eventTypeSource.path` below points at the field per the + * FACTS hypothesis, but automated extraction from it will not distinguish message vs status types. + */ +export const entry: ProviderRegistryEntry = { + id: "whatsapp", + label: "WhatsApp (Meta)", + category: "communication", + docsUrl: "https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks/", + preset: "github", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "entry.0.changes.0.field" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/woocommerce.ts b/internal-packages/webhook-sources/src/registry/woocommerce.ts new file mode 100644 index 00000000000..0332ab77df9 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/woocommerce.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "woocommerce", + label: "WooCommerce", + category: "commerce", + docsUrl: "https://woocommerce.com/document/webhooks/", + secretProvisioning: "integrator", + eventTypeSource: { from: "header", name: "x-wc-webhook-topic" }, + sampleSource: "hookdeck", +}; diff --git a/internal-packages/webhook-sources/src/registry/workos.ts b/internal-packages/webhook-sources/src/registry/workos.ts new file mode 100644 index 00000000000..879d7255b37 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/workos.ts @@ -0,0 +1,16 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * WorkOS signs webhooks with a `WorkOS-Signature` header formatted as `t=,v1=`, HMAC-SHA256 + * over `{t}.{rawBody}`. That scheme matches our `stripe` preset's algorithm but not its header name + * (`stripe-signature`), so this ships sample-only until a custom verifier config exists for it. + */ +export const entry: ProviderRegistryEntry = { + id: "workos", + label: "WorkOS", + category: "auth-identity", + docsUrl: "https://workos.com/docs/events/data-syncing/webhooks", + secretProvisioning: "provider", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/zendesk.ts b/internal-packages/webhook-sources/src/registry/zendesk.ts new file mode 100644 index 00000000000..3bf8c027617 --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/zendesk.ts @@ -0,0 +1,24 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +/** + * Zendesk signs webhooks with an `X-Zendesk-Webhook-Signature` header: base64(HMAC-SHA256(timestamp + + * body)), paired with an `X-Zendesk-Webhook-Timestamp` header carrying the timestamp. That + * concatenated-no-separator scheme matches none of our presets (closest is `svix`, which needs a + * dot-joined `{id}.{timestamp}.{body}` string and a `webhook-id` header Zendesk never sends), so this + * ships sample-only. + * + * The request body has no fixed schema either: Zendesk triggers/automations send whatever JSON an + * admin authors from content placeholders (e.g. `{{ticket.id}}`, `{{ticket.status}}`, rendered with + * real values before delivery), so there is no universal discriminant field. `status` is the closest + * ever-present, varying field across a common ticket-shaped body; each sample's own `eventType` is the + * authoritative label regardless of what `status` holds. + */ +export const entry: ProviderRegistryEntry = { + id: "zendesk", + label: "Zendesk", + category: "support", + docsUrl: "https://developer.zendesk.com/documentation/webhooks/verifying/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "status" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/registry/zoom.ts b/internal-packages/webhook-sources/src/registry/zoom.ts new file mode 100644 index 00000000000..e673247f11c --- /dev/null +++ b/internal-packages/webhook-sources/src/registry/zoom.ts @@ -0,0 +1,11 @@ +import { type ProviderRegistryEntry } from "./types.js"; + +export const entry: ProviderRegistryEntry = { + id: "zoom", + label: "Zoom", + category: "communication", + docsUrl: "https://developers.zoom.us/docs/api/webhooks/", + secretProvisioning: "integrator", + eventTypeSource: { from: "body", path: "event" }, + sampleSource: "handauthored", +}; diff --git a/internal-packages/webhook-sources/src/roundtrip.test.ts b/internal-packages/webhook-sources/src/roundtrip.test.ts new file mode 100644 index 00000000000..6f8c697f923 --- /dev/null +++ b/internal-packages/webhook-sources/src/roundtrip.test.ts @@ -0,0 +1,95 @@ +import { signWithVerifierConfig, verify } from "@internal/webhook-engine"; +import { + discordVerifierConfig, + githubVerifierConfig, + squareVerifierConfig, + stripeVerifierConfig, + svixVerifierConfig, + webhookProviderConfigs, + type WebhookPresetId, + type WebhookVerifierConfig, +} from "@trigger.dev/core/webhooks"; +import { type SampleRecord as SampleRecordType } from "./sampleRecord.js"; +import { describe, expect, it } from "vitest"; +import { SampleRecord } from "./sampleRecord.js"; +import { samples } from "./samples.js"; + +const TEST_SECRET = "whsec_test_secret_for_roundtrip_only"; + +function configForPreset(presetId: WebhookPresetId): WebhookVerifierConfig | null { + switch (presetId) { + case "stripe": + return stripeVerifierConfig(); + case "github": + return githubVerifierConfig(); + case "svix": + return svixVerifierConfig(); + case "square": + return squareVerifierConfig(); + case "discord": + return discordVerifierConfig(); + case "custom": + return null; + } +} + +describe("webhook-sources schema", () => { + it("every bundled sample matches the SampleRecord schema", () => { + expect(samples.length).toBeGreaterThan(0); + for (const sample of samples) { + SampleRecord.parse(sample); + } + }); +}); + +function configForSample(sample: SampleRecordType): WebhookVerifierConfig | null { + const provider = webhookProviderConfigs[sample.provider as keyof typeof webhookProviderConfigs]; + if (provider) return provider.config(); + if (sample.presetId) return configForPreset(sample.presetId); + return null; +} + +describe("verifier round-trip (sign -> verify)", () => { + const verifiableSamples = samples.filter( + (sample) => sample.presetId || sample.provider in webhookProviderConfigs + ); + + it("has verifiable samples to check", () => { + expect(verifiableSamples.length).toBeGreaterThan(0); + }); + + for (const sample of verifiableSamples) { + it(`${sample.provider} / ${sample.eventType} signs and verifies under its config`, () => { + const config = configForSample(sample); + if (!config) return; + + const rawBody = new TextEncoder().encode(JSON.stringify(sample.body)); + const url = "https://example.com/webhooks/v1/ingest/op_roundtrip"; + + const signed = signWithVerifierConfig({ + config, + secret: TEST_SECRET, + rawBody, + url, + headers: { ...(sample.extraHeaders ?? {}) }, + }); + + if (!signed.ok) { + expect(signed.notSignable).toBe(true); + return; + } + + const verdict = verify( + { kind: "config", config }, + { + rawBytes: signed.body, + headers: signed.headers, + url: signed.url, + secret: TEST_SECRET, + } + ); + + expect(verdict.ok).toBe(true); + }); + } +}); diff --git a/internal-packages/webhook-sources/src/sampleRecord.ts b/internal-packages/webhook-sources/src/sampleRecord.ts new file mode 100644 index 00000000000..9a5fcb7d3bc --- /dev/null +++ b/internal-packages/webhook-sources/src/sampleRecord.ts @@ -0,0 +1,36 @@ +import { WEBHOOK_PRESET_IDS } from "@trigger.dev/core/webhooks"; +import { z } from "zod"; + +/** + * Where a sample body came from. `snapshotDate` (YYYY-MM) drives a staleness flag; `upstream` samples + * are ingested from a maintained source (their `ref` names it), `captured`/`handauthored` otherwise. + */ +export const SampleProvenance = z.object({ + kind: z.enum(["upstream", "captured", "handauthored"]), + ref: z.string().optional(), + snapshotDate: z.string().optional(), +}); +export type SampleProvenance = z.infer; + +/** + * A single provider sample: the event BODY (plus any non-signature routing header). `provider` is a + * freeform id (e.g. "shopify"); `presetId` is set only when the provider maps to one of our verifier + * presets, which unlocks the sign/verify round-trip guarantee. The composer signs the body with the + * endpoint's own config at send time, so the signature is never stored per-sample. + */ +export const SampleRecord = z.object({ + provider: z.string(), + providerLabel: z.string().optional(), + presetId: z.enum(WEBHOOK_PRESET_IDS).optional(), + eventType: z.string(), + name: z.string(), + description: z.string().optional(), + body: z.unknown(), + extraHeaders: z.record(z.string()).optional(), + docsUrl: z.string().optional(), + provenance: SampleProvenance, +}); +export type SampleRecord = z.infer; + +/** The picker metadata (no body), for listing providers + event types. */ +export type SampleManifestItem = Omit; diff --git a/internal-packages/webhook-sources/src/samples.ts b/internal-packages/webhook-sources/src/samples.ts new file mode 100644 index 00000000000..86d696ca645 --- /dev/null +++ b/internal-packages/webhook-sources/src/samples.ts @@ -0,0 +1,16 @@ +import generated from "./generated/hookdeck-samples.json"; +import { handAuthoredSamples } from "./handAuthored/index.js"; +import { type SampleRecord } from "./sampleRecord.js"; + +/** + * The provider samples: the vendored Hookdeck ingest (MIT, see NOTICE.md; regenerate with + * `pnpm run ingest`) merged with the hand-authored set, sorted by provider then event type. + */ +export const samples: SampleRecord[] = [ + ...(generated as unknown as SampleRecord[]), + ...handAuthoredSamples, +].sort((a, b) => + a.provider === b.provider + ? a.eventType.localeCompare(b.eventType) + : a.provider.localeCompare(b.provider) +); diff --git a/internal-packages/webhook-sources/tsconfig.json b/internal-packages/webhook-sources/tsconfig.json new file mode 100644 index 00000000000..725f269f23a --- /dev/null +++ b/internal-packages/webhook-sources/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "Node16", + "moduleResolution": "Node16", + "moduleDetection": "force", + "verbatimModuleSyntax": false, + "types": ["vitest/globals"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "paths": { + "@trigger.dev/core/webhooks": ["../../packages/core/src/v3/webhooks/index"] + } + }, + "exclude": ["node_modules"] +} diff --git a/internal-packages/webhook-sources/vitest.config.ts b/internal-packages/webhook-sources/vitest.config.ts new file mode 100644 index 00000000000..dea05b96d5d --- /dev/null +++ b/internal-packages/webhook-sources/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + }, + esbuild: { + target: "node18", + }, +}); diff --git a/packages/cli-v3/src/dev/devSupervisor.ts b/packages/cli-v3/src/dev/devSupervisor.ts index 6a0d1888afd..e8ba2d4afaf 100644 --- a/packages/cli-v3/src/dev/devSupervisor.ts +++ b/packages/cli-v3/src/dev/devSupervisor.ts @@ -32,7 +32,7 @@ import type { CliApiClient } from "../apiClient.js"; import { copySkillFolders } from "../build/bundleSkills.js"; import type { DevCommandOptions } from "../commands/dev.js"; import { DevRunController } from "../entryPoints/dev-run-controller.js"; -import { cliLink, prettyError } from "../utilities/cliOutput.js"; +import { cliLink, prettyError, prettyWarning } from "../utilities/cliOutput.js"; import { devBranchPathSegment } from "../utilities/devBranch.js"; import { eventBus } from "../utilities/eventBus.js"; import { resolveLocalEnvVars } from "../utilities/localEnvVars.js"; @@ -382,6 +382,16 @@ class DevSupervisor implements WorkerRuntime { return; } + // Non-blocking nudge: a chat.event that no agent lists routes nothing. Common (and fine) + // mid-development while you wire up the chat.agent, so warn rather than fail. + const unclaimedSessionWebhooks = backgroundWorker.manifest.unclaimedSessionWebhooks ?? []; + if (unclaimedSessionWebhooks.length > 0) { + prettyWarning( + `Unclaimed chat.event: ${unclaimedSessionWebhooks.join(", ")}`, + "Not listed on any agent's `events: [...]`, so deliveries won't be routed anywhere. Add each to a chat.agent once you wire it up." + ); + } + const sourceFiles = resolveSourceFiles(manifest.sources, backgroundWorker.manifest.tasks); const backgroundWorkerBody: CreateBackgroundWorkerRequestBody = { @@ -391,6 +401,7 @@ class DevSupervisor implements WorkerRuntime { cliPackageVersion: manifest.cliPackageVersion, tasks: backgroundWorker.manifest.tasks, prompts: backgroundWorker.manifest.prompts, + webhooks: backgroundWorker.manifest.webhooks, queues: backgroundWorker.manifest.queues, contentHash: manifest.contentHash, sourceFiles, diff --git a/packages/cli-v3/src/entryPoints/dev-index-worker.ts b/packages/cli-v3/src/entryPoints/dev-index-worker.ts index 59228f0971d..bf83ff8d720 100644 --- a/packages/cli-v3/src/entryPoints/dev-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-index-worker.ts @@ -195,6 +195,8 @@ await sendMessageInCatalog( tasks, prompts: convertPromptSchemasToJsonSchemas(resourceCatalog.listPromptManifests()), skills: resourceCatalog.listSkillManifests(), + webhooks: resourceCatalog.listWebhookManifests(), + unclaimedSessionWebhooks: resourceCatalog.listUnclaimedSessionWebhooks(), queues: resourceCatalog.listQueueManifests(), configPath: buildManifest.configPath, runtime: buildManifest.runtime, diff --git a/packages/cli-v3/src/entryPoints/managed-index-worker.ts b/packages/cli-v3/src/entryPoints/managed-index-worker.ts index f463c4156e5..8b3f2665518 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-worker.ts @@ -191,6 +191,8 @@ await sendMessageInCatalog( tasks, prompts: convertPromptSchemasToJsonSchemas(resourceCatalog.listPromptManifests()), skills: resourceCatalog.listSkillManifests(), + webhooks: resourceCatalog.listWebhookManifests(), + unclaimedSessionWebhooks: resourceCatalog.listUnclaimedSessionWebhooks(), queues: resourceCatalog.listQueueManifests(), configPath: buildManifest.configPath, runtime: buildManifest.runtime, diff --git a/packages/core/package.json b/packages/core/package.json index f0c3adfcf2f..fc691856ed0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,6 +25,7 @@ "./logger": "./src/logger.ts", "./utils": "./src/utils.ts", "./schemas": "./src/schemas/index.ts", + "./webhooks": "./src/v3/webhooks/index.ts", "./types": "./src/types.ts", "./v3": "./src/v3/index.ts", "./v3/tracer": "./src/v3/tracer.ts", @@ -292,6 +293,17 @@ "default": "./dist/commonjs/schemas/index.js" } }, + "./webhooks": { + "import": { + "@triggerdotdev/source": "./src/v3/webhooks/index.ts", + "types": "./dist/esm/v3/webhooks/index.d.ts", + "default": "./dist/esm/v3/webhooks/index.js" + }, + "require": { + "types": "./dist/commonjs/v3/webhooks/index.d.ts", + "default": "./dist/commonjs/v3/webhooks/index.js" + } + }, "./types": { "import": { "@triggerdotdev/source": "./src/types.ts", diff --git a/packages/core/src/v3/isomorphic/friendlyId.ts b/packages/core/src/v3/isomorphic/friendlyId.ts index 3141b75aecd..854d015069e 100644 --- a/packages/core/src/v3/isomorphic/friendlyId.ts +++ b/packages/core/src/v3/isomorphic/friendlyId.ts @@ -267,6 +267,8 @@ export const BulkActionId = new IdUtil("bulk"); export const AttemptId = new IdUtil("attempt"); export const ErrorId = new IdUtil("error"); export const SessionId = new IdUtil("session"); +export const WebhookEndpointId = new IdUtil("wh"); // wh_... +export const WebhookDeliveryId = new IdUtil("whd"); // whd_... export class IdGenerator { private alphabet: string; diff --git a/packages/core/src/v3/resource-catalog/catalog.ts b/packages/core/src/v3/resource-catalog/catalog.ts index 33f66e237bb..1295b3e9144 100644 --- a/packages/core/src/v3/resource-catalog/catalog.ts +++ b/packages/core/src/v3/resource-catalog/catalog.ts @@ -4,6 +4,8 @@ import type { SkillManifest, SkillMetadata, TaskManifest, + WebhookManifest, + WebhookMetadata, WorkerManifest, } from "../schemas/index.js"; import type { @@ -33,4 +35,13 @@ export interface ResourceCatalog { registerSkillMetadata(skill: SkillMetadata): void; listSkillManifests(): Array; getSkillManifest(id: string): SkillManifest | undefined; + registerWebhookMetadata(webhook: WebhookMetadata): void; + listWebhookManifests(): Array; + getWebhookManifest(id: string): WebhookManifest | undefined; + listWebhookIdCollisions(): Array<{ id: string; filePaths: string[] }>; + // session.webhook descriptors that were declared vs claimed by an agent's `webhooks: [...]`. A + // declared-but-unclaimed descriptor routes nothing, so the indexer surfaces it (fail loud). + registerDeclaredSessionWebhook(id: string): void; + markSessionWebhookClaimed(id: string): void; + listUnclaimedSessionWebhooks(): Array; } diff --git a/packages/core/src/v3/resource-catalog/index.ts b/packages/core/src/v3/resource-catalog/index.ts index cd5446b1fba..41211892e2c 100644 --- a/packages/core/src/v3/resource-catalog/index.ts +++ b/packages/core/src/v3/resource-catalog/index.ts @@ -6,6 +6,8 @@ import type { SkillManifest, SkillMetadata, TaskManifest, + WebhookManifest, + WebhookMetadata, WorkerManifest, } from "../schemas/index.js"; import type { @@ -120,6 +122,28 @@ export class ResourceCatalogAPI { return this.#getCatalog().getSkillManifest(id); } + public registerWebhookMetadata(webhook: WebhookMetadata): void { + this.#getCatalog().registerWebhookMetadata(webhook); + } + public listWebhookManifests(): Array { + return this.#getCatalog().listWebhookManifests(); + } + public getWebhookManifest(id: string): WebhookManifest | undefined { + return this.#getCatalog().getWebhookManifest(id); + } + public listWebhookIdCollisions(): Array<{ id: string; filePaths: string[] }> { + return this.#getCatalog().listWebhookIdCollisions(); + } + public registerDeclaredSessionWebhook(id: string): void { + this.#getCatalog().registerDeclaredSessionWebhook(id); + } + public markSessionWebhookClaimed(id: string): void { + this.#getCatalog().markSessionWebhookClaimed(id); + } + public listUnclaimedSessionWebhooks(): Array { + return this.#getCatalog().listUnclaimedSessionWebhooks(); + } + #getCatalog(): ResourceCatalog { return getGlobal(API_NAME) ?? NOOP_RESOURCE_CATALOG; } diff --git a/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts b/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts index a78ccf4539e..5fe4d2144d9 100644 --- a/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts +++ b/packages/core/src/v3/resource-catalog/noopResourceCatalog.ts @@ -4,6 +4,8 @@ import type { SkillManifest, SkillMetadata, TaskManifest, + WebhookManifest, + WebhookMetadata, WorkerManifest, } from "../schemas/index.js"; import { @@ -97,4 +99,26 @@ export class NoopResourceCatalog implements ResourceCatalog { getSkillManifest(id: string): SkillManifest | undefined { return undefined; } + + registerWebhookMetadata(webhook: WebhookMetadata): void { + // noop + } + listWebhookManifests(): Array { + return []; + } + getWebhookManifest(id: string): WebhookManifest | undefined { + return undefined; + } + listWebhookIdCollisions(): Array<{ id: string; filePaths: string[] }> { + return []; + } + registerDeclaredSessionWebhook(id: string): void { + // noop + } + markSessionWebhookClaimed(id: string): void { + // noop + } + listUnclaimedSessionWebhooks(): Array { + return []; + } } diff --git a/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts b/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts index b5cf70bb0d0..16fb665332b 100644 --- a/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts +++ b/packages/core/src/v3/resource-catalog/standardResourceCatalog.ts @@ -6,6 +6,8 @@ import type { TaskFileMetadata, TaskMetadata, TaskManifest, + WebhookManifest, + WebhookMetadata, WorkerManifest, QueueManifest, } from "../schemas/index.js"; @@ -41,6 +43,11 @@ export class StandardResourceCatalog implements ResourceCatalog { private _queueMetadata: Map = new Map(); private _skillMetadata: Map = new Map(); private _skillFileMetadata: Map = new Map(); + private _webhookMetadata: Map = new Map(); + private _webhookFileMetadata: Map = new Map(); + private _webhookIdCollisions: Array<{ id: string; filePaths: string[] }> = []; + private _declaredSessionWebhooks: Set = new Set(); + private _claimedSessionWebhooks: Set = new Set(); private _sentinelContextWarned: Set = new Set(); // Task ids registered more than once (across files and task types). Tasks are // keyed by id below, so a second registration silently overwrites the first; @@ -355,6 +362,70 @@ export class StandardResourceCatalog implements ResourceCatalog { }; } + registerWebhookMetadata(webhook: WebhookMetadata): void { + if (!this._currentFileContext) { + return; + } + if (!webhook.id) { + return; + } + + if ( + this._webhookMetadata.has(webhook.id) && + this._currentFileContext.filePath !== NO_FILE_CONTEXT + ) { + const existingFilePath = this._webhookFileMetadata.get(webhook.id)?.filePath; + const currentFilePath = this._currentFileContext.filePath; + const collision = this._webhookIdCollisions.find((c) => c.id === webhook.id); + if (collision) { + collision.filePaths.push(currentFilePath); + } else { + this._webhookIdCollisions.push({ + id: webhook.id, + filePaths: [existingFilePath ?? currentFilePath, currentFilePath], + }); + } + } + + this._webhookFileMetadata.set(webhook.id, { ...this._currentFileContext }); + this._webhookMetadata.set(webhook.id, webhook); + } + + listWebhookIdCollisions(): Array<{ id: string; filePaths: string[] }> { + return this._webhookIdCollisions; + } + + registerDeclaredSessionWebhook(id: string): void { + if (!id) return; + this._declaredSessionWebhooks.add(id); + } + + markSessionWebhookClaimed(id: string): void { + if (!id) return; + this._claimedSessionWebhooks.add(id); + } + + listUnclaimedSessionWebhooks(): Array { + return [...this._declaredSessionWebhooks].filter((id) => !this._claimedSessionWebhooks.has(id)); + } + + listWebhookManifests(): Array { + const result: Array = []; + for (const [id, metadata] of this._webhookMetadata) { + const fileMetadata = this._webhookFileMetadata.get(id); + if (!fileMetadata) continue; + result.push({ ...metadata, ...fileMetadata }); + } + return result; + } + + getWebhookManifest(id: string): WebhookManifest | undefined { + const metadata = this._webhookMetadata.get(id); + const fileMetadata = this._webhookFileMetadata.get(id); + if (!metadata || !fileMetadata) return undefined; + return { ...metadata, ...fileMetadata }; + } + disable() { // noop } diff --git a/packages/core/src/v3/schemas/build.ts b/packages/core/src/v3/schemas/build.ts index 52cae0b2385..dd4b019003d 100644 --- a/packages/core/src/v3/schemas/build.ts +++ b/packages/core/src/v3/schemas/build.ts @@ -1,6 +1,13 @@ import { z } from "zod"; import { ConfigManifest } from "./config.js"; -import { PromptManifest, QueueManifest, SkillManifest, TaskFile, TaskManifest } from "./schemas.js"; +import { + PromptManifest, + QueueManifest, + SkillManifest, + TaskFile, + TaskManifest, + WebhookManifest, +} from "./schemas.js"; export const BuildExternal = z.object({ name: z.string(), @@ -105,6 +112,8 @@ export const WorkerManifest = z.object({ tasks: TaskManifest.array(), prompts: PromptManifest.array().optional(), skills: SkillManifest.array().optional(), + webhooks: WebhookManifest.array().optional(), // NEW + unclaimedSessionWebhooks: z.array(z.string()).optional(), // session.webhook descriptors no agent listed queues: QueueManifest.array().optional(), workerEntryPoint: z.string(), controllerEntryPoint: z.string().optional(), diff --git a/packages/core/src/v3/schemas/index.ts b/packages/core/src/v3/schemas/index.ts index 8587ed04e04..0e6255038cc 100644 --- a/packages/core/src/v3/schemas/index.ts +++ b/packages/core/src/v3/schemas/index.ts @@ -12,6 +12,9 @@ export * from "./config.js"; export * from "./build.js"; export * from "./runEngine.js"; export * from "./webhooks.js"; +export * from "./webhookConfig.js"; +export * from "./webhookApi.js"; +export * from "./webhookFilter.js"; export * from "./checkpoints.js"; export * from "./warmStart.js"; export * from "./queues.js"; diff --git a/packages/core/src/v3/schemas/resources.ts b/packages/core/src/v3/schemas/resources.ts index 753324d1257..bc0b7a74d8c 100644 --- a/packages/core/src/v3/schemas/resources.ts +++ b/packages/core/src/v3/schemas/resources.ts @@ -1,6 +1,11 @@ import { z } from "zod"; import { QueueManifest, RetryOptions, ScheduleMetadata } from "./schemas.js"; import { MachineConfig } from "./common.js"; +import { + WebhookVerifierArtifact, + WebhookRoutingTarget, + WebhookSecretProvisioning, +} from "./webhookConfig.js"; export const AgentConfig = z.object({ type: z.string(), // "ai-sdk-chat" initially, extensible for future agent types @@ -53,12 +58,27 @@ export const PromptResource = z.object({ export type PromptResource = z.infer; +export const WebhookResource = z.object({ + id: z.string(), + description: z.string().optional(), + filePath: z.string(), + exportName: z.string().optional(), + source: z.string(), + verifierArtifact: WebhookVerifierArtifact, + routingTarget: WebhookRoutingTarget, + secretProvisioning: WebhookSecretProvisioning.optional(), + filter: z.string().optional(), // delivery filter DSL string; compiled to a FilterAst at deploy-sync + metadata: z.record(z.unknown()).optional(), +}); +export type WebhookResource = z.infer; + export const BackgroundWorkerMetadata = z.object({ packageVersion: z.string(), contentHash: z.string(), cliPackageVersion: z.string().optional(), tasks: z.array(TaskResource), prompts: z.array(PromptResource).optional(), + webhooks: z.array(WebhookResource).optional(), // NEW queues: z.array(QueueManifest).optional(), sourceFiles: z.array(BackgroundWorkerSourceFileMetadata).optional(), runtime: z.string().optional(), diff --git a/packages/core/src/v3/schemas/schemas.ts b/packages/core/src/v3/schemas/schemas.ts index 0b12e7ae3db..dde7449bc4b 100644 --- a/packages/core/src/v3/schemas/schemas.ts +++ b/packages/core/src/v3/schemas/schemas.ts @@ -1,5 +1,10 @@ import { z } from "zod"; import type { RequireKeys } from "../types/index.js"; +import { + WebhookVerifierArtifact, + WebhookRoutingTarget, + WebhookSecretProvisioning, +} from "./webhookConfig.js"; import { MachineConfig, MachinePreset, @@ -268,6 +273,28 @@ export const SkillManifest = z.object({ }); export type SkillManifest = z.infer; +// ── Webhooks ──────────────────────────────────────────────────────────────── + +const webhookMetadata = { + id: z.string(), + description: z.string().optional(), + source: z.string(), + verifierArtifact: WebhookVerifierArtifact, + routingTarget: WebhookRoutingTarget, + secretProvisioning: WebhookSecretProvisioning.optional(), + filter: z.string().optional(), // delivery filter DSL string; compiled to a FilterAst at deploy-sync + metadata: z.record(z.unknown()).optional(), +}; + +export const WebhookMetadata = z.object(webhookMetadata); +export type WebhookMetadata = z.infer; + +export const WebhookManifest = z.object({ + ...webhookMetadata, + ...taskFileMetadata, // filePath, exportName?, entryPoint +}); +export type WebhookManifest = z.infer; + export const PostStartCauses = z.enum(["index", "create", "restore"]); export type PostStartCauses = z.infer; diff --git a/packages/core/src/v3/schemas/webhookApi.ts b/packages/core/src/v3/schemas/webhookApi.ts new file mode 100644 index 00000000000..95d49251cdd --- /dev/null +++ b/packages/core/src/v3/schemas/webhookApi.ts @@ -0,0 +1,106 @@ +import { z } from "zod"; +// Reuse the source-side enum (provider | integrator | either) rather than redefining it. +import { WebhookSecretProvisioning } from "./webhookConfig.js"; + +// Public HTTP API objects for webhooks (endpoints + deliveries). Read surface for now; the create +// path (dynamic endpoints) and lifecycle actions layer on later. Mirrors the Errors API shape: +// list endpoints return `{ data, pagination? }`, detail endpoints return the object directly. +// Statuses are exposed lowercase over the API (the DB enums are uppercase). + +export const WebhookEndpointApiStatus = z.enum(["active", "inactive", "deleting"]); +export type WebhookEndpointApiStatus = z.infer; + +export const WebhookDeliveryApiStatus = z.enum([ + "pending", + "processing", + "succeeded", + "failed", + "filtered", +]); +export type WebhookDeliveryApiStatus = z.infer; + +export const WebhookEndpointObject = z.object({ + /** Stable friendly id, e.g. `wh_...`. */ + id: z.string(), + /** The declared webhook() id this endpoint routes to. */ + webhook: z.string(), + /** Provider tag, e.g. "stripe" | "github" | "standard". */ + source: z.string(), + status: WebhookEndpointApiStatus, + /** Who supplies the secret/key; drives paste-vs-generate in the dashboard. */ + secretProvisioning: WebhookSecretProvisioning, + /** Whether a signing secret/public key has been set. The value is never returned. */ + secretSet: z.boolean(), + /** Tenant scope (P2 dynamic endpoints); null for the declared default endpoint. */ + tenantId: z.string().nullable(), + externalRef: z.string().nullable(), + /** The hosted ingress URL to point the provider at. */ + url: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), +}); +export type WebhookEndpointObject = z.infer; + +export const ListWebhookEndpointsResponse = z.object({ + data: z.array(WebhookEndpointObject), +}); +export type ListWebhookEndpointsResponse = z.infer; + +export const WebhookDeliveryListItem = z.object({ + /** Stable friendly id, e.g. `whd_...`. */ + id: z.string(), + /** The webhook() id this delivery was routed to, if resolvable. */ + webhook: z.string().nullable(), + status: WebhookDeliveryApiStatus, + /** Provider delivery id (Stripe event id, GitHub X-GitHub-Delivery, …). */ + externalDeliveryId: z.string(), + /** The triggered run's friendly id, if any. */ + runId: z.string().nullable(), + createdAt: z.coerce.date(), + processedAt: z.coerce.date().nullable(), +}); +export type WebhookDeliveryListItem = z.infer; + +export const ListWebhookDeliveriesResponse = z.object({ + data: z.array(WebhookDeliveryListItem), + pagination: z.object({ + next: z.string().optional(), + previous: z.string().optional(), + }), +}); +export type ListWebhookDeliveriesResponse = z.infer; + +export const WebhookDeliveryObject = WebhookDeliveryListItem.extend({ + idempotencyKey: z.string(), + /** The size-capped, verified event body. */ + event: z.unknown().nullable(), + /** The inbound request headers. */ + headers: z.record(z.string()).nullable(), + rawBodyHash: z.string().nullable(), + error: z.string().nullable(), + /** For a `filtered` delivery: why it was not routed (failing clause + actual value). */ + filterReason: z.string().nullable(), + updatedAt: z.coerce.date(), +}); +export type WebhookDeliveryObject = z.infer; + +// ── Write actions ── + +/** Rotating/setting a signing secret returns the plaintext ONCE; it is never readable again. */ +export const RotateWebhookEndpointSecretResponse = z.object({ + id: z.string(), + secretSet: z.literal(true), + secret: z.string(), +}); +export type RotateWebhookEndpointSecretResponse = z.infer< + typeof RotateWebhookEndpointSecretResponse +>; + +/** Replaying re-runs a delivery's task from its stored event as a new delivery. */ +export const ReplayWebhookDeliveryResponse = z.object({ + /** The new delivery's friendly id (GET it once processed for the run). */ + deliveryId: z.string(), + /** The original delivery id the replay was created from. */ + replayedFrom: z.string(), +}); +export type ReplayWebhookDeliveryResponse = z.infer; diff --git a/packages/core/src/v3/schemas/webhookConfig.ts b/packages/core/src/v3/schemas/webhookConfig.ts new file mode 100644 index 00000000000..281955a6b7d --- /dev/null +++ b/packages/core/src/v3/schemas/webhookConfig.ts @@ -0,0 +1,214 @@ +import { z } from "zod"; + +// Ingress webhook verification config. Kept in a LEAF module (imports only `z`) so it +// can be consumed by resources.ts / schemas.ts without dragging in the alert-webhook +// `Webhook` union in webhooks.ts, which imports api.js (RunStatus). resources.ts is +// imported by api.ts, so a resources -> webhooks -> api edge would be a module-init cycle. + +// Who supplies the signing secret / key, driving the dashboard Connect UI (NOT a provider-name +// switch — it's data on the source). "provider": the provider generates it and you paste it in +// (Stripe, Slack, Svix). "integrator": you choose it, so the dashboard can generate-and-reveal one +// for you to paste into the provider (GitHub, GitLab). "either": offer both. +export const WebhookSecretProvisioning = z.enum(["provider", "integrator", "either"]); +export type WebhookSecretProvisioning = z.infer; + +// Q9: the provider delivery id can live in a header (GitHub x-github-delivery) or the body (Stripe id). +export const WebhookIdempotencyField = z.object({ + from: z.enum(["header", "body"]), + name: z.string(), +}); +export type WebhookIdempotencyField = z.infer; + +// ── Shared, data-only verification primitives (no per-provider code) ── + +// Where a scalar value comes from. Used for the timestamp and for signing-string template vars. +// "signatureField" reads a field parsed out of the signature header (e.g. Stripe `t`). +// "url" is the inbound request URL (e.g. Square signs `{url}{body}`). "constant" is a literal. +export const WebhookValueSource = z.discriminatedUnion("from", [ + z.object({ from: z.literal("header"), name: z.string() }), + z.object({ from: z.literal("signatureField"), field: z.string() }), + z.object({ from: z.literal("body"), path: z.string() }), + z.object({ from: z.literal("url") }), + z.object({ from: z.literal("constant"), value: z.string() }), +]); +export type WebhookValueSource = z.infer; + +// How to pull the signature value(s) out of one header. All fields optional → the whole +// header value IS the signature (Shopify, Linear). With fieldSeparator+field we select +// named element(s): GitHub `sha256=`, Stripe `t=…,v1=…`, Svix `v1, v1,`. +// Field extraction is multi-valued (rotation / signature lists) → the verifier accepts ANY match. +export const WebhookSignatureExtraction = z.object({ + itemSeparator: z.string().optional(), // split header into elements: Stripe "," · Svix " " + fieldSeparator: z.string().optional(), // split element into namevalue: "=" · Svix "," + field: z.string().optional(), // element name carrying the signature: "v1" | "sha256" | "v0" + trim: z.boolean().optional(), // trim each element (WorkOS "t=…, v1=…") +}); +export type WebhookSignatureExtraction = z.infer; + +// Replay protection + a `{timestamp}` template var. Source is a header, an in-signature field +// (Stripe `t`), or a body field (Linear `webhookTimestamp`). Unit defaults to seconds. +export const WebhookTimestampConfig = z.object({ + source: WebhookValueSource, + unit: z.enum(["seconds", "milliseconds"]).optional(), + toleranceSeconds: z.number().optional(), +}); +export type WebhookTimestampConfig = z.infer; + +// The stored credential transform. Most providers use the secret as-is (utf8). Svix-family +// secrets are `whsec_` and must be base64-decoded after stripping the prefix. +export const WebhookSecretTransform = z.object({ + encoding: z.enum(["utf8", "base64"]).optional(), + stripPrefix: z.string().optional(), +}); +export type WebhookSecretTransform = z.infer; + +// The bytes that get signed. "raw" = the body verbatim. Otherwise a template with `{body}`, +// `{timestamp}` (auto-bound) and any named `vars` (e.g. `{id}` for Svix, `{url}` for Square). +export const WebhookSigningString = z.union([ + z.literal("raw"), + z.object({ + template: z.string(), + vars: z.record(WebhookValueSource).optional(), + }), +]); +export type WebhookSigningString = z.infer; + +/** + * Body-parsing hint. Some providers send an event as JSON but a SECOND kind of callback (e.g. Slack + * interactivity) as `application/x-www-form-urlencoded` with the JSON in a single field (Slack: `payload`). + * When JSON parsing fails, the engine form-decodes and JSON-parses `field`'s value. The signature is over + * the raw body either way, so verification is unaffected. Generic (a field name), no provider code in the engine. + */ +export const WebhookFormPayload = z.object({ + field: z.string(), +}); +export type WebhookFormPayload = z.infer; + +export const WebhookHmacConfig = z.object({ + scheme: z.literal("hmac"), + algorithm: z.enum(["sha256", "sha1"]), + encoding: z.enum(["hex", "base64"]), // explicit, no auto-detect (design decision 3) + signatureHeader: z.string(), + signature: WebhookSignatureExtraction.optional(), // omit = bare whole-header value + timestamp: WebhookTimestampConfig.optional(), + signingString: WebhookSigningString, + secret: WebhookSecretTransform.optional(), // omit = utf8, no prefix strip + idempotencyField: WebhookIdempotencyField.optional(), + formPayload: WebhookFormPayload.optional(), +}); +export type WebhookHmacConfig = z.infer; + +// Asymmetric (public-key) signatures: the provider signs with a private key, we verify with +// their public key. Same parsing/template primitives as HMAC; only the final verify differs. +// Covers Discord (ed25519), SendGrid (ecdsa-p256), RSA providers. Dynamic key fetch (PayPal +// cert URL / JWKS) is out of scope → the custom-bundle escape hatch. +export const WebhookAsymmetricConfig = z.object({ + scheme: z.literal("asymmetric"), + algorithm: z.enum(["ed25519", "ecdsa-p256", "rsa-sha256"]), + encoding: z.enum(["hex", "base64"]), // how the signature value is encoded + signatureHeader: z.string(), + signature: WebhookSignatureExtraction.optional(), + timestamp: WebhookTimestampConfig.optional(), + signingString: WebhookSigningString, + // How the stored public key is encoded: a PEM string, base64 of DER SPKI (SendGrid), + // or raw key bytes (ed25519 raw, Discord). Defaults to "pem". + publicKeyEncoding: z.enum(["pem", "spki-der-base64", "raw-hex", "raw-base64"]).optional(), + idempotencyField: WebhookIdempotencyField.optional(), +}); +export type WebhookAsymmetricConfig = z.infer; + +export const WebhookSharedSecretConfig = z.object({ + scheme: z.literal("shared-secret"), + placement: z.enum(["header", "bearer", "basic", "body"]), + fieldName: z.string().optional(), + idempotencyField: WebhookIdempotencyField.optional(), +}); +export type WebhookSharedSecretConfig = z.infer; + +export const WebhookUrlSecretConfig = z.object({ + scheme: z.literal("url-secret"), + placement: z.enum(["query", "path"]), + paramName: z.string(), + idempotencyField: WebhookIdempotencyField.optional(), +}); +export type WebhookUrlSecretConfig = z.infer; + +export const WebhookVerifierConfig = z.discriminatedUnion("scheme", [ + WebhookHmacConfig, + WebhookSharedSecretConfig, + WebhookUrlSecretConfig, + WebhookAsymmetricConfig, +]); +export type WebhookVerifierConfig = z.infer; + +// ── Provider handshake: a signed request that must get a synchronous echo, not a recorded/routed +// delivery (Slack url_verification, Discord PING). Generic + data-only: if the verified body's +// `matchPath` equals `matchValue`, ingest responds 200 with the body's `respondPath` value. ── +export const WebhookHandshakeConfig = z.object({ + matchPath: z.string(), // dotted path into the body, e.g. "type" + matchValue: z.string(), // e.g. "url_verification" + respondPath: z.string(), // dotted path to echo, e.g. "challenge" +}); +export type WebhookHandshakeConfig = z.infer; + +// ── Verifier artifact: data-only tagged union stored on WebhookEndpoint.verifierArtifact ── +export const WebhookVerifierArtifact = z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("config"), + config: WebhookVerifierConfig, + handshake: WebhookHandshakeConfig.optional(), + }), + z.object({ + kind: z.literal("preset"), + preset: z.string(), + config: WebhookVerifierConfig, + handshake: WebhookHandshakeConfig.optional(), + }), + z.object({ kind: z.literal("bundle"), bundleUrl: z.string(), hash: z.string() }), // P3 seam +]); +export type WebhookVerifierArtifact = z.infer; + +// ── Routing target: data-only tagged union stored on WebhookEndpoint.routingTarget ── +// P1 implements only { type: "task" }. The session variant routes a delivery to a find-or-created +// session: keyTemplate resolves the externalId, deliverAs selects the mode. "action" (chat.event) +// carries actionType → the onAction envelope's action.type; "message" (channels) carries connectorId +// → the run resolves the connector's inbound() mapper and runs a turn. +export const WebhookRoutingTarget = z.discriminatedUnion("type", [ + z.object({ type: z.literal("task"), taskId: z.string() }), + z.object({ + type: z.literal("session"), + taskIdentifier: z.string(), + keyTemplate: z.string(), + deliverAs: z.enum(["action", "message"]), + actionType: z.string().optional(), + connectorId: z.string().optional(), + triggerConfigTemplate: z.record(z.unknown()).optional(), + // Gate session CREATION: an event that already resolves to an existing session always resumes it, + // but a key with no session is only started when the event matches this filter. Absent => always start. + startOn: z.string().optional(), + }), +]); +export type WebhookRoutingTarget = z.infer; + +// ── Data-only verdict the engine verifier returns (M5 consumes) ── +export const WebhookVerifierResult = z.object({ + ok: z.boolean(), + idempotencyKey: z.string(), + parsedEvent: z.unknown().optional(), + error: z.string().optional(), +}); +export type WebhookVerifierResult = z.infer; + +// ── Preset event-type unions (P1 ships these TYPE-only) ── +export type StripeWebhookEvent = { + id: string; + object: "event"; + type: string; + created: number; + data: { object: Record }; + [k: string]: unknown; +}; + +export type GitHubWebhookEvent = { + [k: string]: unknown; +}; diff --git a/packages/core/src/v3/schemas/webhookFilter.ts b/packages/core/src/v3/schemas/webhookFilter.ts new file mode 100644 index 00000000000..ce60613cc36 --- /dev/null +++ b/packages/core/src/v3/schemas/webhookFilter.ts @@ -0,0 +1,278 @@ +import { z } from "zod"; + +// Webhook delivery filter: server-side predicate gating routing. LEAF module (imports only `z`). + +// ── Runtime AST (stored on WebhookEndpoint.filterAst; produced by the engine parser) ── + +export const FilterOp = z.enum([ + "eq", + "neq", + "gt", + "lt", + "gte", + "lte", + "in", + "nin", + "startsWith", + "endsWith", + "contains", +]); +export type FilterOp = z.infer; + +export const FilterScalar = z.union([z.string(), z.number(), z.boolean(), z.null()]); +export type FilterScalar = z.infer; + +export const FilterValue = z.union([FilterScalar, z.array(FilterScalar)]); +export type FilterValue = z.infer; + +export type FilterAst = + | { kind: "and"; clauses: FilterAst[] } + | { kind: "or"; clauses: FilterAst[] } + // Exactly one of `value` (literal RHS) / `valueRef` (field-to-field: another path on the same event). + | { kind: "clause"; path: string; op: FilterOp; value?: FilterValue; valueRef?: string } + // `path any|all (subClause)`: the array at `path`, with `clause.path` resolved against each element. + | { + kind: "quantifier"; + mode: "any" | "all"; + path: string; + clause: { path: string; op: FilterOp; value: FilterValue }; + }; + +// Bump when the AST shape changes; a stored AST with a different version is re-parsed from `filter`. +export const FILTER_AST_VERSION = 1; + +export const FilterAst: z.ZodType = z.lazy(() => + z.union([ + z.object({ kind: z.literal("and"), clauses: z.array(FilterAst) }), + z.object({ kind: z.literal("or"), clauses: z.array(FilterAst) }), + z.object({ + kind: z.literal("clause"), + path: z.string(), + op: FilterOp, + value: FilterValue.optional(), + valueRef: z.string().optional(), + }), + z.object({ + kind: z.literal("quantifier"), + mode: z.enum(["any", "all"]), + path: z.string(), + clause: z.object({ path: z.string(), op: FilterOp, value: FilterValue }), + }), + ]) +); + +// The endpoint-level metadata the `webhook.*` namespace resolves against (mirrors WebhookEndpoint). +export type WebhookFilterMeta = { + externalRef: string; + tenantId: string; + id: string; + source: string; + deliveryId: string; +}; + +// ── Compile-time validator (type-only; the SDK `webhook({ filter })` generic uses it) ── +// Tail-recursive scanner so TS's tail-recursion elimination applies (ceiling ~200+ clauses, not ~12). + +type FilterError = `✖ ${M}`; +type Invalid = { readonly "~webhookFilterInvalid": true }; + +type PathValue = P extends `${infer H}.${infer R}` + ? H extends keyof T + ? PathValue, R> + : Invalid + : P extends keyof T + ? NonNullable + : Invalid; + +type ResolveFilterPath = P extends `event.${infer R}` + ? PathValue + : P extends `header.${string}` + ? string + : P extends `webhook.${infer R}` + ? PathValue + : Invalid; + +type FilterLiteralType = Lit extends `'${infer S}'` + ? S + : Lit extends `${infer N extends number}` + ? N + : Lit extends "true" + ? true + : Lit extends "false" + ? false + : Lit extends "null" + ? null + : Invalid; + +type CheckValue = [FilterLiteralType] extends [ + Invalid, +] + ? FilterError<`cannot parse value: ${Lit}`> + : [FilterLiteralType] extends [F] + ? true + : FilterError<`value ${Lit} is not valid for ${P}`>; +type CheckNum = [F] extends [number] + ? Lit extends `${number}` + ? true + : FilterError<`${P} needs a number`> + : FilterError<`${P} is not numeric`>; +type CheckStr = [F] extends [string] + ? Lit extends `'${string}'` + ? true + : FilterError<`${P} op needs a string literal`> + : FilterError<`${P} is not a string`>; +// `in`/`not in` lists are validated unspaced (`['a','b']`), since the type tokenizer is space-only; +// the runtime tokenizer also accepts spaced lists. +type CheckInItems< + F, + Items extends string, + P extends string, +> = Items extends `${infer Head},${infer Tail}` + ? CheckValue extends true + ? CheckInItems + : CheckValue + : CheckValue; +type CheckIn = Lit extends `[${infer Items}]` + ? CheckInItems + : FilterError<`${P} "in" expects a [list]`>; +type CheckOp = Op extends "==" | "!=" + ? CheckValue + : Op extends ">" | "<" | ">=" | "<=" + ? CheckNum + : Op extends "startsWith" | "endsWith" | "contains" + ? CheckStr + : Op extends "in" + ? CheckIn + : FilterError<`unknown operator: ${Op}`>; +// An unquoted, namespace-prefixed operand is a field reference (field-to-field), not a literal. +type IsFilterPath = S extends + | `event.${string}` + | `header.${string}` + | `webhook.${string}` + ? true + : false; + +// Field-to-field: only equality/ordering, and the two field types must be comparable. +type CheckFieldToField< + TEvent, + F, + Op extends string, + Rhs extends string, + P extends string, +> = Op extends "==" | "!=" | ">" | "<" | ">=" | "<=" + ? ResolveFilterPath extends infer RF + ? [RF] extends [Invalid] + ? FilterError<`unknown field: ${Rhs}`> + : [RF] extends [F] + ? true + : [F] extends [RF] + ? true + : FilterError<`${P} and ${Rhs} are not comparable`> + : never + : FilterError<`operator "${Op}" does not support a field reference`>; + +type LeafCheck = + ResolveFilterPath extends infer F + ? [F] extends [Invalid] + ? FilterError<`unknown field: ${P}`> + : IsFilterPath extends true + ? CheckFieldToField + : CheckOp + : never; + +// Quantifier: `arrPath any|all ( subPath op lit )` — the sub-clause resolves against the array element. +type ElementOf = T extends readonly (infer E)[] ? E : never; +type CheckQuantifier< + TEvent, + ArrP extends string, + SubP extends string, + Op extends string, + Lit extends string, +> = + ResolveFilterPath extends infer Arr + ? [Arr] extends [Invalid] + ? FilterError<`unknown field: ${ArrP}`> + : [ElementOf] extends [never] + ? FilterError<`${ArrP} is not an array`> + : PathValue, SubP> extends infer SF + ? [SF] extends [Invalid] + ? FilterError<`unknown field: ${SubP} (in ${ArrP})`> + : CheckOp + : never + : never; + +type Tokenize = S extends `${infer H} ${infer R}` + ? Tokenize + : [...Acc, S]; + +type Scan< + TEvent, + Tk extends string[], + Depth extends 1[], + Expect extends "operand" | "operator", +> = Expect extends "operand" + ? Tk extends ["(", ...infer Rest extends string[]] + ? Scan + : Tk extends [")" | "&&" | "||", ...string[]] + ? FilterError<`unexpected "${Tk[0] & string}"`> + : Tk extends [ + infer ArrP extends string, + "any" | "all", + "(", + infer SubP extends string, + infer SubOp extends string, + infer SubLit extends string, + ")", + ...infer Rest extends string[], + ] + ? CheckQuantifier extends infer R + ? R extends true + ? Scan + : R + : never + : Tk extends [ + infer P extends string, + "not", + "in", + infer Lit extends string, + ...infer Rest extends string[], + ] + ? ResolveFilterPath extends infer F + ? [F] extends [Invalid] + ? FilterError<`unknown field: ${P}`> + : CheckIn extends infer R + ? R extends true + ? Scan + : R + : never + : never + : Tk extends [ + infer P extends string, + infer Op extends string, + infer Lit extends string, + ...infer Rest extends string[], + ] + ? LeafCheck extends infer R + ? R extends true + ? Scan + : R + : never + : FilterError<`incomplete clause`> + : Tk extends [] + ? Depth extends [] + ? true + : FilterError<`unbalanced parens (missing ")")`> + : Tk extends ["&&" | "||", ...infer Rest extends string[]] + ? Scan + : Tk extends [")", ...infer Rest extends string[]] + ? Depth extends [1, ...infer D extends 1[]] + ? Scan + : FilterError<`unmatched ")"`> + : FilterError<`expected && or ||, got "${Tk[0] & string}"`>; + +type ScanFilter = Scan, [], "operand">; + +// Valid -> S; invalid -> a branded FilterError literal. Use with `const S` in conditional-param +// position so the error surfaces inline: `filter?: ValidateWebhookFilter`. +export type ValidateWebhookFilter = + ScanFilter extends true ? S : ScanFilter; diff --git a/packages/core/src/v3/schemas/webhooks.ts b/packages/core/src/v3/schemas/webhooks.ts index b5ed927602e..1008e7ad700 100644 --- a/packages/core/src/v3/schemas/webhooks.ts +++ b/packages/core/src/v3/schemas/webhooks.ts @@ -289,3 +289,8 @@ export type RunFailedWebhook = Extract; export type DeploymentSuccessWebhook = Extract; export type DeploymentFailedWebhook = Extract; export type ErrorWebhook = Extract; + +// The ingress webhook verification schemas live in ./webhookConfig.ts (a leaf module), +// re-exported via schemas/index.ts. They are deliberately NOT here: this file imports +// api.js (RunStatus) for the alert union, and resources.ts (imported by api.ts) needs +// those config schemas, so keeping them here would create a module-init cycle. diff --git a/packages/core/src/v3/types/chatEvents.ts b/packages/core/src/v3/types/chatEvents.ts new file mode 100644 index 00000000000..1b2d28aa5cd --- /dev/null +++ b/packages/core/src/v3/types/chatEvents.ts @@ -0,0 +1,112 @@ +// Type machinery for `chat.event(...)` — the descriptor an agent claims via `chat.agent({ events })`. +// Type-only. The `key` is a validated string of {body.x}/{webhook.x}/{header.x} placeholders. +import type { + WebhookSecretProvisioning, + WebhookVerifierArtifact, +} from "../schemas/webhookConfig.js"; + +export type WebhookKeyScalar = string | number | boolean | bigint; + +// Endpoint metadata resolvable at ingest without parsing the body. externalRef/tenantId are the "" +// sentinel on declared (P1) webhooks; per-tenant with P2 endpoints. +export interface WebhookKeyMeta { + externalRef: string; // the "webhook external id" (endpointExternalRef); the multi-tenant scope + tenantId: string; + id: string; // handlerWebhookId + source: string; + deliveryId: string; +} + +// A miss must be a real brand, not `never` (`never extends WebhookKeyScalar` is `true`, so bad paths +// would pass). +declare const invalidBrand: unique symbol; +type Invalid = { readonly [invalidBrand]: true }; + +type PathValue = P extends `${infer Head}.${infer Rest}` + ? Head extends keyof T + ? PathValue, Rest> + : Invalid + : P extends keyof T + ? NonNullable + : Invalid; + +export type WebhookKeyError = `✖ ${M}`; + +type TrimSpace = S extends ` ${infer R}` + ? TrimSpace + : S extends `${infer L} ` + ? TrimSpace + : S; + +// Validate one {…} placeholder by namespace: webhook.→WebhookKeyMeta, header.→any non-empty name, +// body./bare→event. `true` when a scalar path, else a branded error naming it. +type CheckKeyPathSingle = Path extends `webhook.${infer Rest}` + ? PathValue extends WebhookKeyScalar + ? true + : WebhookKeyError<`unknown or non-scalar webhook meta path: ${Rest}`> + : Path extends `header.${infer Name}` + ? Name extends "" + ? WebhookKeyError<`empty header name`> + : true + : Path extends `body.${infer Rest}` + ? PathValue extends WebhookKeyScalar + ? true + : WebhookKeyError<`unknown or non-scalar event path: ${Rest}`> + : PathValue extends WebhookKeyScalar + ? true + : WebhookKeyError<`unknown or non-scalar event path: ${Path}`>; + +// A placeholder may list first-non-empty fallbacks: `{a || b}`. Validate each side as a path. +type CheckKeyPath = Path extends `${infer L}||${infer R}` + ? CheckKeyPathSingle> extends true + ? CheckKeyPath> + : CheckKeyPathSingle> + : CheckKeyPathSingle>; + +type CheckKeyTemplate< + TEvent, + S extends string, +> = S extends `${infer _Pre}{${infer Path}}${infer Rest}` + ? CheckKeyPath extends true + ? CheckKeyTemplate + : CheckKeyPath + : true; + +// The parameter type IS the validation result: S when valid, else the branded error (which localizes +// the message to `key`). No union, no `S & ...` intersection — both broke inference/errors before. +// Paired with a `const S extends string` fn generic that captures the literal from the argument. +export type ValidatedWebhookKey = + CheckKeyTemplate extends true ? S : CheckKeyTemplate; + +declare const dEvt: unique symbol; + +export interface ChatEvent { + id: string; + type: TType; // the action.type discriminant; defaults to `id` when omitted at authoring + key: string; // compiled canonical template + source: string; // provider tag + // Registration data carried from the source so the claiming agent can create the endpoint. + verifierArtifact: WebhookVerifierArtifact; + secretProvisioning?: WebhookSecretProvisioning; + // Optional server-side gate (same DSL as webhook()); a non-matching delivery is recorded FILTERED + // and never routed to the session. + filter?: string; + readonly [dEvt]?: TEvent; +} + +export type AnyChatEvent = ChatEvent; + +// The fixed envelope a delivery becomes, projected from a descriptor; drives chat.agent.onAction. +export type ChatEventAction = + W extends ChatEvent + ? { + type: TType; + event: TEvent; + source: string; + headers: Record; + deliveryId: string; + } + : never; + +// Distributive union over an agent's `events` tuple (closed `type` discriminant, `event` narrows). +export type ChatEventActions = ChatEventAction; diff --git a/packages/core/src/v3/types/index.ts b/packages/core/src/v3/types/index.ts index 05f9473a82f..d7d0f3c2a1d 100644 --- a/packages/core/src/v3/types/index.ts +++ b/packages/core/src/v3/types/index.ts @@ -8,6 +8,8 @@ export * from "./queues.js"; export * from "./tasks.js"; export * from "./tools.js"; export * from "./utils.js"; +export * from "./webhooks.js"; +export * from "./chatEvents.js"; type ResolveEnvironmentVariablesOptions = { variables: Record | Array<{ name: string; value: string }>; diff --git a/packages/core/src/v3/types/webhooks.ts b/packages/core/src/v3/types/webhooks.ts new file mode 100644 index 00000000000..52071e8db99 --- /dev/null +++ b/packages/core/src/v3/types/webhooks.ts @@ -0,0 +1,39 @@ +import type { + WebhookVerifierArtifact, + WebhookRoutingTarget, + WebhookSecretProvisioning, +} from "../schemas/webhookConfig.js"; + +declare const __webhookEvent: unique symbol; + +export type WebhookSource = { + /** provider tag, e.g. "stripe" | "github" | "custom" */ + provider: string; + /** data-only verifier artifact (config | preset in P1) */ + verifier: WebhookVerifierArtifact; + /** who supplies the secret/key; drives the Connect UI (paste vs generate). Defaults to "either". */ + secretProvisioning?: WebhookSecretProvisioning; + /** phantom, type-level only; never present at runtime */ + [__webhookEvent]?: TEvent; +}; + +export type AnyWebhookSource = WebhookSource; + +export type InferWebhookEvent = S extends WebhookSource ? TEvent : unknown; + +// The envelope the platform delivers to a webhook task run: the verified event body plus the +// inbound request headers. The SDK's webhook() run unwraps this into onEvent({ event, headers }). +// Kept here so the trigger side (webapp) and the receive side (SDK) agree on the shape. +export type WebhookRunPayload = { + event: TEvent; + headers: Record; +}; + +// ── P2 seam: TYPE ONLY, no runtime function ── +export type CreateWebhookEndpointParams = { + /** declared webhook() id (string ref; GOLDEN LAW, never the value) */ + handler: string; + tenantId?: string; + externalRef?: string; + metadata?: Record; +}; diff --git a/packages/core/src/v3/webhooks/index.ts b/packages/core/src/v3/webhooks/index.ts new file mode 100644 index 00000000000..202852ca2be --- /dev/null +++ b/packages/core/src/v3/webhooks/index.ts @@ -0,0 +1,295 @@ +import type { + WebhookAsymmetricConfig, + WebhookHmacConfig, + WebhookSecretProvisioning, + WebhookVerifierConfig, +} from "../schemas/webhookConfig.js"; + +/** + * Preset verifier-config builders: the single source of truth for how each supported provider signs. + * The public SDK `webhooks.stripe()` producers wrap these (attaching provider + secretProvisioning + + * the phantom event type), and `@internal/webhook-sources` uses them to sign-and-verify its samples. + * These carry zero SDK dependency (pure `WebhookVerifierConfig` data), so they live in core. + */ + +export function stripeVerifierConfig(opts?: { toleranceSeconds?: number }): WebhookHmacConfig { + return { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "stripe-signature", + signature: { itemSeparator: ",", fieldSeparator: "=", field: "v1" }, + timestamp: { + source: { from: "signatureField", field: "t" }, + toleranceSeconds: opts?.toleranceSeconds ?? 300, + }, + signingString: { template: "{timestamp}.{body}" }, + idempotencyField: { from: "body", name: "id" }, + }; +} + +export function githubVerifierConfig(): WebhookHmacConfig { + return { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-hub-signature-256", + signature: { fieldSeparator: "=", field: "sha256" }, + signingString: "raw", + idempotencyField: { from: "header", name: "x-github-delivery" }, + }; +} + +export function svixVerifierConfig(): WebhookHmacConfig { + return { + scheme: "hmac", + algorithm: "sha256", + encoding: "base64", + signatureHeader: "svix-signature", + signature: { itemSeparator: " ", fieldSeparator: ",", field: "v1" }, + timestamp: { source: { from: "header", name: "svix-timestamp" }, toleranceSeconds: 300 }, + signingString: { + template: "{id}.{timestamp}.{body}", + vars: { id: { from: "header", name: "svix-id" } }, + }, + secret: { encoding: "base64", stripPrefix: "whsec_" }, + idempotencyField: { from: "header", name: "svix-id" }, + }; +} + +export function squareVerifierConfig(): WebhookHmacConfig { + return { + scheme: "hmac", + algorithm: "sha256", + encoding: "base64", + signatureHeader: "x-square-hmacsha256-signature", + signature: {}, + signingString: { template: "{url}{body}", vars: { url: { from: "url" } } }, + idempotencyField: { from: "body", name: "event_id" }, + }; +} + +export function discordVerifierConfig(opts?: { + publicKeyEncoding?: "raw-hex" | "pem"; +}): WebhookAsymmetricConfig { + return { + scheme: "asymmetric", + algorithm: "ed25519", + encoding: "hex", + signatureHeader: "x-signature-ed25519", + signature: {}, + timestamp: { source: { from: "header", name: "x-signature-timestamp" } }, + signingString: { template: "{timestamp}{body}" }, + publicKeyEncoding: opts?.publicKeyEncoding ?? "raw-hex", + }; +} + +export const WEBHOOK_PRESET_IDS = [ + "custom", + "stripe", + "github", + "svix", + "square", + "discord", +] as const; +export type WebhookPresetId = (typeof WEBHOOK_PRESET_IDS)[number]; + +/** + * Generic HMAC verifier-config builder. The per-provider configs below are concise declarative calls; + * each names the signature header, how the signature (and optional timestamp) is parsed out of it, and + * the bytes that get signed. The engine signer/verifier are the single implementation. + */ +function hmacVerifierConfig(opts: { + header: string; + algorithm?: "sha256" | "sha1"; + encoding?: "hex" | "base64"; + field?: string; + fieldSeparator?: string; + itemSeparator?: string; + trim?: boolean; + signingTemplate?: string; + timestamp?: + | { from: "header"; name: string; unit?: "seconds" | "milliseconds" } + | { from: "signatureField"; field: string; unit?: "seconds" | "milliseconds" }; + toleranceSeconds?: number; +}): WebhookHmacConfig { + const signature: WebhookHmacConfig["signature"] = {}; + if (opts.itemSeparator) signature.itemSeparator = opts.itemSeparator; + if (opts.field) { + signature.fieldSeparator = opts.fieldSeparator ?? "="; + signature.field = opts.field; + } + if (opts.trim) signature.trim = true; + + const config: WebhookHmacConfig = { + scheme: "hmac", + algorithm: opts.algorithm ?? "sha256", + encoding: opts.encoding ?? "hex", + signatureHeader: opts.header, + signature, + signingString: opts.signingTemplate ? { template: opts.signingTemplate } : "raw", + }; + + if (opts.timestamp) { + config.timestamp = { + source: + opts.timestamp.from === "header" + ? { from: "header", name: opts.timestamp.name } + : { from: "signatureField", field: opts.timestamp.field }, + ...(opts.timestamp.unit ? { unit: opts.timestamp.unit } : {}), + ...(opts.toleranceSeconds ? { toleranceSeconds: opts.toleranceSeconds } : {}), + }; + } + + return config; +} + +/** + * Per-provider verifier configs for providers whose signature is a plain HMAC variant that maps onto + * the config primitives (a different header name / prefix / signing template than the shared presets, + * but no new verifier capability). Each is round-trip tested in @internal/webhook-sources and wrapped + * by a `webhooks.X()` SDK producer. Providers needing asymmetric verification, hex-decoded keys, or + * URL+param signing are NOT here (they ship sample-only until the verifier gains those). + */ +export const webhookProviderConfigs = { + slack: { + secretProvisioning: "provider", + config: () => + hmacVerifierConfig({ + header: "x-slack-signature", + field: "v0", + signingTemplate: "v0:{timestamp}:{body}", + timestamp: { from: "header", name: "x-slack-request-timestamp" }, + toleranceSeconds: 300, + }), + }, + zoom: { + secretProvisioning: "provider", + config: () => + hmacVerifierConfig({ + header: "x-zm-signature", + field: "v0", + signingTemplate: "v0:{timestamp}:{body}", + timestamp: { from: "header", name: "x-zm-request-timestamp" }, + toleranceSeconds: 300, + }), + }, + calendly: { + secretProvisioning: "provider", + config: () => + hmacVerifierConfig({ + header: "calendly-webhook-signature", + itemSeparator: ",", + field: "v1", + signingTemplate: "{timestamp}.{body}", + timestamp: { from: "signatureField", field: "t" }, + toleranceSeconds: 300, + }), + }, + "cal-com": { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-cal-signature-256" }), + }, + sentry: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "sentry-hook-signature" }), + }, + pagerduty: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-pagerduty-signature", field: "v1" }), + }, + vercel: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-vercel-signature", algorithm: "sha1" }), + }, + linear: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "linear-signature" }), + }, + notion: { + secretProvisioning: "provider", + config: () => hmacVerifierConfig({ header: "x-notion-signature", field: "sha256" }), + }, + typeform: { + secretProvisioning: "integrator", + config: () => + hmacVerifierConfig({ header: "typeform-signature", field: "sha256", encoding: "base64" }), + }, + docusign: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-docusign-signature-1", encoding: "base64" }), + }, + jira: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-hub-signature", field: "sha256" }), + }, + intercom: { + secretProvisioning: "integrator", + config: () => + hmacVerifierConfig({ header: "x-hub-signature", field: "sha1", algorithm: "sha1" }), + }, + zendesk: { + secretProvisioning: "integrator", + config: () => + hmacVerifierConfig({ + header: "x-zendesk-webhook-signature", + encoding: "base64", + signingTemplate: "{timestamp}{body}", + timestamp: { from: "header", name: "x-zendesk-webhook-signature-timestamp" }, + }), + }, + attio: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "attio-signature" }), + }, + workos: { + secretProvisioning: "provider", + config: () => + hmacVerifierConfig({ + header: "workos-signature", + itemSeparator: ",", + field: "v1", + trim: true, + signingTemplate: "{timestamp}.{body}", + timestamp: { from: "signatureField", field: "t", unit: "milliseconds" }, + }), + }, + elevenlabs: { + secretProvisioning: "provider", + config: () => + hmacVerifierConfig({ + header: "elevenlabs-signature", + itemSeparator: ",", + field: "v0", + signingTemplate: "{timestamp}.{body}", + timestamp: { from: "signatureField", field: "t" }, + toleranceSeconds: 1800, + }), + }, + vapi: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-vapi-signature" }), + }, + retell: { + secretProvisioning: "provider", + config: () => + hmacVerifierConfig({ + header: "x-retell-signature", + itemSeparator: ",", + field: "d", + signingTemplate: "{body}{timestamp}", + timestamp: { from: "signatureField", field: "v", unit: "milliseconds" }, + }), + }, + shopify: { + secretProvisioning: "integrator", + config: () => hmacVerifierConfig({ header: "x-shopify-hmac-sha256", encoding: "base64" }), + }, +} satisfies Record< + string, + { secretProvisioning: WebhookSecretProvisioning; config: () => WebhookVerifierConfig } +>; + +export type WebhookProviderId = keyof typeof webhookProviderConfigs; + +export * from "../schemas/webhookConfig.js"; diff --git a/packages/slack/package.json b/packages/slack/package.json new file mode 100644 index 00000000000..ee258950a11 --- /dev/null +++ b/packages/slack/package.json @@ -0,0 +1,74 @@ +{ + "name": "@trigger.dev/slack", + "version": "4.5.0-rc.7", + "description": "Slack chat frontend (channel) for trigger.dev agents", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/triggerdotdev/trigger.dev", + "directory": "packages/slack" + }, + "type": "module", + "files": [ + "dist" + ], + "tshy": { + "selfLink": false, + "main": true, + "module": true, + "project": "./tsconfig.src.json", + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + }, + "sourceDialects": [ + "@triggerdotdev/source" + ] + }, + "scripts": { + "clean": "rimraf dist .tshy .tshy-build .turbo", + "build": "tshy && pnpm run update-version", + "dev": "tshy --watch", + "typecheck": "tsc --noEmit -p tsconfig.src.json", + "test": "vitest", + "update-version": "tsx ../../scripts/updateVersion.ts", + "check-exports": "attw --pack ." + }, + "dependencies": { + "@trigger.dev/core": "workspace:4.5.0-rc.7" + }, + "peerDependencies": { + "@trigger.dev/sdk": "workspace:^4.5.0-rc.7" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.15.4", + "@trigger.dev/sdk": "workspace:4.5.0-rc.7", + "rimraf": "6.0.1", + "tshy": "^3.0.2", + "tsx": "4.17.0", + "vitest": "^2.1.9" + }, + "engines": { + "node": ">=18.20.0" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@triggerdotdev/source": "./src/index.ts", + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } + }, + "main": "./dist/commonjs/index.js", + "types": "./dist/commonjs/index.d.ts", + "module": "./dist/esm/index.js" +} diff --git a/packages/slack/src/index.test.ts b/packages/slack/src/index.test.ts new file mode 100644 index 00000000000..2777eac61c9 --- /dev/null +++ b/packages/slack/src/index.test.ts @@ -0,0 +1,307 @@ +import { describe, expect, it, vi } from "vitest"; +import { mentions, slack, toSlackMrkdwn, type SlackMessageEvent } from "./index.js"; + +const messageEvent = ( + over: Partial> = {} +): SlackMessageEvent => ({ + type: "event_callback", + event: { type: "message", channel: "C9", ts: "1699999999.0001", text: "hi", ...over }, +}); + +describe("slack channel", () => { + it("default inbound strips a leading bot mention", () => { + const c = slack({ id: "s1", token: "xoxb-t" }); + expect(c.inbound(messageEvent({ text: "<@U123> hello there" }))).toBe("hello there"); + expect(c.inbound(messageEvent({ text: "plain" }))).toBe("plain"); + }); + + it("composes the self-message guard with a user filter, and always admits interactivity", () => { + const guardOnly = slack({ id: "s2", token: "t" }); + expect(guardOnly.filter).toContain("event.event.type == 'message'"); + expect(guardOnly.filter).toContain("event.event.bot_id == null"); + expect(guardOnly.filter).toContain( + "event.event.subtype in [null, 'file_share', 'thread_broadcast']" + ); + expect(guardOnly.filter).toContain("event.type == 'block_actions'"); + + const withUser = slack({ id: "s3", token: "t", filter: "event.event.channel == 'C1'" }); + expect(withUser.filter).toContain("&& (event.event.channel == 'C1')"); + expect(withUser.filter).toContain("event.type == 'block_actions'"); + }); + + it("keys one session per thread, converging message events and interactivity", () => { + const c = slack({ id: "s4", token: "t" }); + expect(c.key).toBe( + "{body.team_id || body.team.id}:{body.event.channel || body.container.channel_id}:{body.event.thread_ts || body.event.ts || body.container.thread_ts || body.container.message_ts}" + ); + }); + + it("renderInteraction produces Block Kit approve/deny buttons carrying the toolCallId", () => { + const c = slack({ id: "s-hitl", token: "t" }); + const msg = c.renderInteraction?.( + [{ toolCallId: "call-1", toolName: "requestApproval", input: { amount: 50 } }], + { + event: messageEvent(), + deliveryId: "d1", + } + ); + const values = (msg?.blocks as any[]).flatMap((b) => b.elements ?? []).map((e: any) => e.value); + expect(values).toContain("call-1::approve"); + expect(values).toContain("call-1::deny"); + }); + + it("onInteraction resolves a block_actions click to a tool output; ignores messages", () => { + const c = slack({ id: "s-hitl2", token: "t" }); + const approve = c.onInteraction?.({ + type: "block_actions", + actions: [{ value: "call-9::approve" }], + } as never); + expect(approve).toEqual({ toolCallId: "call-9", output: { approved: true } }); + + const deny = c.onInteraction?.({ + type: "block_actions", + actions: [{ value: "call-9::deny" }], + } as never); + expect(deny).toEqual({ toolCallId: "call-9", output: { approved: false } }); + + expect(c.onInteraction?.(messageEvent())).toBeNull(); + }); + + it("finalizeInteraction replaces the controls via response_url, dropping the buttons", async () => { + const calls: Array<{ url: string; body: Record }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string, init: { body: string }) => { + calls.push({ url, body: JSON.parse(init.body) }); + return { json: async () => ({ ok: true }) }; + }) + ); + const c = slack({ id: "s-fin", token: "t" }); + await c.finalizeInteraction?.( + { + type: "block_actions", + user: { id: "U42" }, + response_url: "https://hooks.slack.test/r/1", + actions: [{ value: "call-1::approve" }], + message: { + blocks: [ + { type: "section", text: { type: "mrkdwn", text: "Approval needed" } }, + { type: "actions", elements: [{ type: "button", value: "call-1::approve" }] }, + ], + }, + } as never, + { toolCallId: "call-1", output: { approved: true } } + ); + expect(calls[0]?.url).toBe("https://hooks.slack.test/r/1"); + expect(calls[0]?.body.replace_original).toBe(true); + const types = (calls[0]?.body.blocks as Array<{ type: string }>).map((b) => b.type); + expect(types).not.toContain("actions"); + expect(JSON.stringify(calls[0]?.body.blocks)).toContain("Approved"); + vi.unstubAllGlobals(); + }); + + it("finalizeInteraction is a no-op without a response_url", async () => { + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + const c = slack({ id: "s-fin2", token: "t" }); + await c.finalizeInteraction?.( + { type: "block_actions", actions: [{ value: "x::deny" }] } as never, + { toolCallId: "x", output: { approved: false } } + ); + expect(fetchMock).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); + + it("passes startOn through verbatim (not composed with the guard)", () => { + const none = slack({ id: "s5", token: "t" }); + expect(none.startOn).toBeUndefined(); + + const summon = slack({ id: "s6", token: "t", startOn: mentions("U012BOT") }); + expect(summon.startOn).toBe( + "(event.event.text contains '<@U012BOT>' || event.event.text contains '<@U012BOT|')" + ); + }); + + it("default ack varies text on crash recovery", () => { + const c = slack({ id: "s7", token: "t" }); + expect(c.ack?.(messageEvent(), { recovered: false })).toEqual({ text: "on it..." }); + expect(c.ack?.(messageEvent(), { recovered: true })).toEqual({ + text: "picking this back up...", + }); + }); + + it("ack: null disables the placeholder", () => { + const c = slack({ id: "s8", token: "t", ack: null }); + expect(c.ack).toBeUndefined(); + }); + + it("a custom ack receives the recovery ctx", () => { + const c = slack({ + id: "s9", + token: "t", + ack: (_e, ctx) => ({ text: ctx.recovered ? "resuming" : "starting" }), + }); + expect(c.ack?.(messageEvent(), { recovered: false })).toEqual({ text: "starting" }); + expect(c.ack?.(messageEvent(), { recovered: true })).toEqual({ text: "resuming" }); + }); + + it("toSlackMrkdwn converts common markdown to Slack mrkdwn", () => { + expect(toSlackMrkdwn("**bold**")).toBe("*bold*"); + expect(toSlackMrkdwn("__bold__")).toBe("*bold*"); + expect(toSlackMrkdwn("## Heading")).toBe("*Heading*"); + expect(toSlackMrkdwn("- one\n- two")).toBe("• one\n• two"); + expect(toSlackMrkdwn("[docs](https://trigger.dev)")).toBe(""); + expect(toSlackMrkdwn("~~gone~~")).toBe("~gone~"); + // A real model reply: heading + bold + bullets in one string. + expect(toSlackMrkdwn("## Help\n\nI can do **stuff**:\n- a\n- b")).toBe( + "*Help*\n\nI can do *stuff*:\n• a\n• b" + ); + }); + + it("mentions() builds a mention predicate for one or many bot ids", () => { + expect(mentions("U1")).toBe( + "(event.event.text contains '<@U1>' || event.event.text contains '<@U1|')" + ); + expect(mentions("U1", "U2")).toContain("<@U1>"); + expect(mentions("U1", "U2")).toContain("<@U2|"); + expect(() => mentions()).toThrow(/at least one/); + }); + + it("send posts then edits, threading the ref and using the bot token", async () => { + const calls: Array<{ url: string; body: Record; auth: unknown }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string, init: { body: string; headers: Record }) => { + calls.push({ url, body: JSON.parse(init.body), auth: init.headers.authorization }); + return { json: async () => ({ ok: true, ts: "1700000000.0001" }) }; + }) + ); + + const c = slack({ id: "s5", token: "xoxb-secret", apiBaseUrl: "https://mock.slack" }); + const event = messageEvent(); + + const ackRes = await c.send!( + { text: "on it..." }, + { + event, + deliveryId: "d1", + mode: "final", + final: false, + } + ); + expect(ackRes.ref).toBe("1700000000.0001"); + expect(calls[0]?.url).toBe("https://mock.slack/chat.postMessage"); + expect(calls[0]?.auth).toBe("Bearer xoxb-secret"); + expect(calls[0]?.body.channel).toBe("C9"); + expect(calls[0]?.body.thread_ts).toBe("1699999999.0001"); + + await c.send!( + { text: "done" }, + { + event, + deliveryId: "d1", + previousRef: ackRes.ref, + mode: "final", + final: true, + } + ); + expect(calls[1]?.url).toBe("https://mock.slack/chat.update"); + expect(calls[1]?.body.ts).toBe("1700000000.0001"); + expect(calls[1]?.body.text).toBe("done"); + + vi.unstubAllGlobals(); + }); + + it("send targets the thread from a block_actions payload (HITL resume egress)", async () => { + const calls: Array<{ body: Record }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (_url: string, init: { body: string }) => { + calls.push({ body: JSON.parse(init.body) }); + return { json: async () => ({ ok: true, ts: "1700000000.9" }) }; + }) + ); + const c = slack({ id: "s-resume", token: "t", apiBaseUrl: "https://mock.slack" }); + const interaction = { + type: "block_actions", + team: { id: "T1" }, + container: { + type: "message", + channel_id: "C42", + thread_ts: "1699999999.0001", + message_ts: "1700000000.5", + }, + actions: [{ value: "call-1::approve" }], + }; + await c.send!( + { text: "refund approved and processed" }, + { + event: interaction as never, + deliveryId: "d-resume", + mode: "final", + final: true, + } + ); + expect(calls[0]?.body.channel).toBe("C42"); + expect(calls[0]?.body.thread_ts).toBe("1699999999.0001"); + vi.unstubAllGlobals(); + }); + + it("send throws when the bot is not in the channel", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ json: async () => ({ ok: false, error: "not_in_channel" }) })) + ); + const c = slack({ id: "s6", token: "t", apiBaseUrl: "https://mock.slack" }); + await expect( + c.send!({ text: "x" }, { event: messageEvent(), deliveryId: "d", mode: "final", final: true }) + ).rejects.toThrow(/not_in_channel/); + vi.unstubAllGlobals(); + }); + + it("react adds/removes an emoji on the triggering message (colons stripped)", async () => { + const calls: Array<{ url: string; body: Record }> = []; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string, init: { body: string }) => { + calls.push({ url, body: JSON.parse(init.body) }); + return { json: async () => ({ ok: true }) }; + }) + ); + const c = slack({ + id: "s7", + token: "xoxb-secret", + apiBaseUrl: "https://mock.slack", + reactions: { working: "eyes", done: "white_check_mark" }, + }); + expect(c.reactions).toEqual({ working: "eyes", done: "white_check_mark" }); + + await c.react!({ name: "eyes" }, { event: messageEvent(), deliveryId: "d1" }); + expect(calls[0]?.url).toBe("https://mock.slack/reactions.add"); + expect(calls[0]?.body).toMatchObject({ + channel: "C9", + timestamp: "1699999999.0001", + name: "eyes", + }); + + await c.react!( + { name: ":white_check_mark:", remove: true }, + { event: messageEvent(), deliveryId: "d1" } + ); + expect(calls[1]?.url).toBe("https://mock.slack/reactions.remove"); + expect(calls[1]?.body.name).toBe("white_check_mark"); + vi.unstubAllGlobals(); + }); + + it("react swallows already_reacted (idempotent)", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ json: async () => ({ ok: false, error: "already_reacted" }) })) + ); + const c = slack({ id: "s8", token: "t", apiBaseUrl: "https://mock.slack" }); + await expect( + c.react!({ name: "eyes" }, { event: messageEvent(), deliveryId: "d1" }) + ).resolves.toBeUndefined(); + vi.unstubAllGlobals(); + }); +}); diff --git a/packages/slack/src/index.ts b/packages/slack/src/index.ts new file mode 100644 index 00000000000..a91c0de8e69 --- /dev/null +++ b/packages/slack/src/index.ts @@ -0,0 +1,409 @@ +import { chat } from "@trigger.dev/sdk/ai"; +import type { + ChannelAckCtx, + ChannelConnector, + ChannelInteractionCtx, + ChannelInteractionResolution, + ChannelMessage, + ChannelMessageInput, + ChannelPendingToolCall, + ChannelReaction, + ChannelReactCtx, + ChannelReactions, + ChannelReply, + ChannelSendCtx, +} from "@trigger.dev/sdk/ai"; +import type { + WebhookHandshakeConfig, + WebhookHmacConfig, + WebhookSource, +} from "@trigger.dev/core/v3"; + +// A minimal Slack Events API envelope; pass your own event type for fuller typing. +export type SlackMessageEvent = { + type: string; + event_id?: string; + team_id?: string; + event?: { + type: string; + subtype?: string; + text?: string; + user?: string; + channel?: string; + ts?: string; + thread_ts?: string; + bot_id?: string; + }; +}; + +// Slack signs `X-Slack-Signature: v0=` over `v0:{timestamp}:{body}`; the timestamp rides in +// `X-Slack-Request-Timestamp`. You paste the Slack signing secret as the endpoint's signing secret. +const SLACK_VERIFIER: WebhookHmacConfig = { + scheme: "hmac", + algorithm: "sha256", + encoding: "hex", + signatureHeader: "x-slack-signature", + signature: { fieldSeparator: "=", field: "v0" }, + timestamp: { + source: { from: "header", name: "x-slack-request-timestamp" }, + toleranceSeconds: 300, + }, + signingString: { template: "v0:{timestamp}:{body}" }, + idempotencyField: { from: "body", name: "event_id" }, + formPayload: { field: "payload" }, +}; + +// Slack's Event Subscriptions url_verification handshake: echo the challenge, do not record/route. +const SLACK_HANDSHAKE: WebhookHandshakeConfig = { + matchPath: "type", + matchValue: "url_verification", + respondPath: "challenge", +}; + +/** + * One session per Slack thread, for BOTH message events and block_actions interactivity (a button click + * on the in-thread ack). thread_ts is only on replies, so a thread-STARTING message falls back to ts; + * interactivity carries the same thread via `container.thread_ts` / `container.message_ts`. The `||` + * operator resolves to the first non-empty path, so both surfaces converge on one externalId. + */ +const DEFAULT_KEY = + "{body.team_id || body.team.id}:{body.event.channel || body.container.channel_id}:{body.event.thread_ts || body.event.ts || body.container.thread_ts || body.container.message_ts}"; + +/** + * Mandatory loop guard for MESSAGE events: `bot_id == null` drops the agent's own posts (no reply loop); + * the subtype allowlist keeps real user messages (absent subtype matches `null`) and drops system/edit + * events. Interactivity (block_actions) is a separate surface, admitted via INTERACTIVITY_PASS. + */ +const SELF_MESSAGE_GUARD = + "event.event.type == 'message' && event.event.bot_id == null && event.event.subtype in [null, 'file_share', 'thread_broadcast']"; + +/** Interactivity callbacks (button clicks) always pass the loop guard; onInteraction resolves them. */ +const INTERACTIVITY_PASS = "event.type == 'block_actions'"; + +const SLACK_API_BASE_URL = "https://slack.com/api"; + +export type SlackToken = string | ((event: TEvent) => string | Promise); + +export type SlackChannelOptions = { + id: string; + /** Bot token (xoxb-...). A string for one workspace, or a resolver keyed on the event's team_id. */ + token: SlackToken; + /** Session key template. Defaults to one session per thread. */ + key?: string; + /** Map a Slack event to the turn's message. Defaults to the message text with the bot mention stripped. */ + inbound?: (event: TEvent) => ChannelMessageInput; + /** Map the agent's reply to a Slack message. Defaults to the reply text (null posts nothing). */ + outbound?: (reply: ChannelReply) => ChannelMessage | null; + /** Placeholder posted while the agent works, then edited to the answer. `null` posts only the answer. */ + ack?: ((event: TEvent, ctx: ChannelAckCtx) => ChannelMessage | null) | null; + /** Extra server-side filter, composed AND with the mandatory self-message guard. */ + filter?: string; + /** + * Only start a NEW session when an event matches this filter; existing threads always resume. Use it + * to summon the bot on mention, then continue the thread silently, e.g. + * `startOn: "event.event.text contains '<@U012BOT>'"` (your bot's user id). + */ + startOn?: string; + /** "final" (default): ack then one edit. "stream": debounced live edits (fast-follow). */ + delivery?: "final" | "stream"; + /** + * Lifecycle emoji reactions on the user's message (names without colons, e.g. "eyes"): `working` is + * added while the turn runs and swapped to `done`, or `error` on failure. Needs the `reactions:write` + * scope. The agent can also react itself via `run({ channel })`. + */ + reactions?: ChannelReactions; + /** Override the Slack Web API base URL (for testing against a mock). */ + apiBaseUrl?: string; +}; + +/** + * Build a predicate that matches when the bot is @mentioned, for `startOn` (summon-on-mention) or + * `filter`. Pass your bot's user id(s) from the Slack app (they start with `U`); matches both the plain + * `<@U012BOT>` and labelled `<@U012BOT|name>` mention forms: + * `slack({ id, token, startOn: mentions("U012BOT") })`. + */ +export function mentions(...botUserIds: string[]): string { + const ids = botUserIds.filter(Boolean); + if (ids.length === 0) throw new Error("mentions() requires at least one bot user id"); + const clauses = ids.flatMap((id) => [ + `event.event.text contains '<@${id}>'`, + `event.event.text contains '<@${id}|'`, + ]); + return `(${clauses.join(" || ")})`; +} + +/** + * Slack as a chat frontend for an agent. List on `chat.agent({ channels: [slack({...})] })`: verified + * Slack messages in a thread are routed to a durable per-thread session and run as turns, and the reply + * is posted back to the thread. Set the endpoint's signing secret to your Slack signing secret; pass the + * bot token as `token`. Subscribe the app to `message.channels` (and invite the bot to the channel). + */ +export function slack( + options: SlackChannelOptions +): ChannelConnector { + const apiBaseUrl = options.apiBaseUrl ?? SLACK_API_BASE_URL; + const source: WebhookSource = { + provider: "slack", + verifier: { kind: "config", config: SLACK_VERIFIER, handshake: SLACK_HANDSHAKE }, + secretProvisioning: "integrator", + }; + const messageFilter = options.filter + ? `${SELF_MESSAGE_GUARD} && (${options.filter})` + : SELF_MESSAGE_GUARD; + const filter = `${INTERACTIVITY_PASS} || (${messageFilter})`; + const ack = + options.ack === null + ? undefined + : (options.ack ?? + ((_event: TEvent, ctx: ChannelAckCtx) => ({ + text: ctx.recovered ? "picking this back up..." : "on it...", + }))); + + return chat.channels.custom>({ + id: options.id, + source, + key: options.key ?? DEFAULT_KEY, + inbound: options.inbound ?? (defaultSlackInbound as (event: TEvent) => ChannelMessageInput), + outbound: options.outbound ?? defaultSlackOutbound, + ack, + send: makeSlackSend(options.token, apiBaseUrl), + renderInteraction: defaultSlackRenderInteraction as ( + pending: ChannelPendingToolCall[], + ctx: ChannelInteractionCtx + ) => ChannelMessage | null, + onInteraction: defaultSlackOnInteraction as ( + event: TEvent + ) => ChannelInteractionResolution | null, + finalizeInteraction: defaultSlackFinalizeInteraction as ( + event: TEvent, + resolution: ChannelInteractionResolution + ) => Promise, + // Composed at runtime (guard + optional user filter), so it bypasses the literal-only filter + // validator; the user's `filter` arg was already validated on the way in. + filter: filter as never, + startOn: options.startOn as never, + delivery: options.delivery ?? "final", + react: makeSlackReact(options.token, apiBaseUrl), + reactions: options.reactions, + }); +} + +/** + * Default HITL controls: render each pending human-decision tool as a Block Kit approve/deny pair. The + * button `value` carries `${toolCallId}::${decision}` so `onInteraction` can resolve the exact tool. + */ +function defaultSlackRenderInteraction( + pending: ChannelPendingToolCall[], + _ctx: ChannelInteractionCtx +): ChannelMessage | null { + const call = pending[0]; + if (!call) return null; + const detail = call.input !== undefined ? "\n```" + safeStringify(call.input) + "```" : ""; + return { + text: `Approval needed: ${call.toolName}`, + blocks: [ + { + type: "section", + text: { type: "mrkdwn", text: `*Approval needed* for \`${call.toolName}\`${detail}` }, + }, + { + type: "actions", + elements: [ + { + type: "button", + action_id: "trigger_hitl_approve", + style: "primary", + text: { type: "plain_text", text: "Approve" }, + value: `${call.toolCallId}::approve`, + }, + { + type: "button", + action_id: "trigger_hitl_deny", + style: "danger", + text: { type: "plain_text", text: "Deny" }, + value: `${call.toolCallId}::deny`, + }, + ], + }, + ], + }; +} + +/** + * Default interaction resolver: a `block_actions` button click resolves the tool named in its `value` + * (`${toolCallId}::approve|deny`) to `{ approved }`. Any non-interactivity event returns null (the + * normal message path handles it). + */ +function defaultSlackOnInteraction(event: unknown): ChannelInteractionResolution | null { + const payload = event as { type?: string; actions?: Array<{ value?: string }> }; + if (payload?.type !== "block_actions") return null; + const action = (payload.actions ?? []).find( + (a) => typeof a?.value === "string" && a.value.includes("::") + ); + if (!action?.value) return null; + const [toolCallId, decision] = action.value.split("::"); + if (!toolCallId || (decision !== "approve" && decision !== "deny")) return null; + return { toolCallId, output: { approved: decision === "approve" } }; +} + +/** + * After a decision, collapse the controls via the interaction's `response_url` (Slack's documented path: + * the click gets a bare 200 ack, then `response_url` accepts `replace_original` for up to 30 minutes). + * Keeps the original context blocks, drops the `actions` block, and appends the outcome, so the buttons + * can't be clicked again. No-op when the payload carries no `response_url` (e.g. a synthetic test event). + */ +async function defaultSlackFinalizeInteraction( + event: unknown, + resolution: ChannelInteractionResolution +): Promise { + const payload = event as { + response_url?: string; + user?: { id?: string }; + message?: { blocks?: unknown[] }; + }; + const responseUrl = payload?.response_url; + if (!responseUrl) return; + + const approved = (resolution.output as { approved?: boolean } | undefined)?.approved === true; + const decision = approved ? "Approved" : "Denied"; + const icon = approved ? ":white_check_mark:" : ":x:"; + const who = payload.user?.id ? ` by <@${payload.user.id}>` : ""; + + const original = Array.isArray(payload.message?.blocks) ? payload.message!.blocks : []; + const kept = original.filter((b) => (b as { type?: string })?.type !== "actions"); + const blocks = [ + ...kept, + { type: "context", elements: [{ type: "mrkdwn", text: `${icon} *${decision}*${who}` }] }, + ]; + + await fetch(responseUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ replace_original: true, text: `${decision}${who}`, blocks }), + }); +} + +function safeStringify(value: unknown): string { + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +// Strip a leading bot mention (`<@U123> hi` -> `hi`) so the agent sees the plain text. +function defaultSlackInbound(event: SlackMessageEvent): string { + return (event.event?.text ?? "").replace(/^\s*<@[A-Z0-9]+>\s*/i, ""); +} + +/** + * Convert common GitHub-flavored markdown (what a model emits) to Slack mrkdwn: `**bold**` -> `*bold*`, + * `#` headings -> bold, `-`/`*` bullets -> `•`, `[t](url)` -> ``, `~~s~~` -> `~s~`. Applied by the + * default outbound; a custom `outbound` controls its own formatting (call this from it if you want it). + * Note: single-asterisk `*italic*` is left as-is, so it renders bold in Slack (rare in model output). + */ +export function toSlackMrkdwn(md: string): string { + return md + .replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<$2|$1>") + .replace(/^[ \t]{0,3}#{1,6}[ \t]+(.+?)[ \t]*#*[ \t]*$/gm, "*$1*") + .replace(/\*\*([^*\n]+)\*\*/g, "*$1*") + .replace(/__([^_\n]+)__/g, "*$1*") + .replace(/~~([^~\n]+)~~/g, "~$1~") + .replace(/^([ \t]*)[-*+][ \t]+/gm, "$1• "); +} + +function defaultSlackOutbound(reply: ChannelReply): ChannelMessage | null { + return reply.text ? { text: toSlackMrkdwn(reply.text) } : null; +} + +function makeSlackSend(token: SlackToken, apiBaseUrl: string) { + return async ( + message: ChannelMessage, + ctx: ChannelSendCtx + ): Promise<{ ref?: string }> => { + const event = ctx.event as SlackMessageEvent & { + container?: { channel_id?: string; thread_ts?: string; message_ts?: string }; + channel?: { id?: string }; + }; + const channel = event.event?.channel ?? event.container?.channel_id ?? event.channel?.id; + const threadTs = + event.event?.thread_ts ?? + event.event?.ts ?? + event.container?.thread_ts ?? + event.container?.message_ts; + + const resolve = () => (typeof token === "function" ? token(ctx.event) : token); + let botToken = await resolve(); + + const blocks = (message as { blocks?: unknown }).blocks; + const rich = Array.isArray(blocks) && blocks.length > 0 ? { blocks } : {}; + + const post = async () => + ctx.previousRef + ? slackApi(apiBaseUrl, "chat.update", botToken, { + channel, + ts: ctx.previousRef, + text: message.text, + ...rich, + }) + : slackApi(apiBaseUrl, "chat.postMessage", botToken, { + channel, + thread_ts: threadTs, + text: message.text, + ...rich, + }); + + let result = await post(); + // Re-resolve once on an auth error (token rotation) when a resolver was supplied. + if (!result.ok && typeof token === "function" && isAuthError(result.error)) { + botToken = await resolve(); + result = await post(); + } + if (!result.ok) { + // not_in_channel / channel_not_found is the common one: the bot isn't in the channel. Surface it. + throw new Error( + `slack ${ctx.previousRef ? "chat.update" : "chat.postMessage"} failed: ${result.error}` + ); + } + return { ref: ctx.previousRef ?? result.ts }; + }; +} + +function isAuthError(error: string | undefined): boolean { + return error === "invalid_auth" || error === "token_revoked" || error === "account_inactive"; +} + +// Add/remove an emoji reaction on the triggering Slack message (needs the reactions:write scope). +function makeSlackReact(token: SlackToken, apiBaseUrl: string) { + return async (reaction: ChannelReaction, ctx: ChannelReactCtx): Promise => { + const event = ctx.event as SlackMessageEvent; + const channel = event.event?.channel; + const timestamp = event.event?.ts; + const name = reaction.name.replace(/^:|:$/g, ""); + if (!channel || !timestamp || !name) return; + const botToken = typeof token === "function" ? await token(ctx.event) : token; + const method = reaction.remove ? "reactions.remove" : "reactions.add"; + const result = await slackApi(apiBaseUrl, method, botToken, { channel, timestamp, name }); + // already_reacted / no_reaction are benign idempotent outcomes; surface anything else. + if (!result.ok && result.error !== "already_reacted" && result.error !== "no_reaction") { + throw new Error(`slack ${method} failed: ${result.error}`); + } + }; +} + +async function slackApi( + baseUrl: string, + method: string, + token: string, + body: Record +): Promise<{ ok: boolean; ts?: string; error?: string }> { + const res = await fetch(`${baseUrl}/${method}`, { + method: "POST", + headers: { + "content-type": "application/json; charset=utf-8", + authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + }); + return (await res.json()) as { ok: boolean; ts?: string; error?: string }; +} diff --git a/packages/slack/tsconfig.json b/packages/slack/tsconfig.json new file mode 100644 index 00000000000..16881b51b6e --- /dev/null +++ b/packages/slack/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../.configs/tsconfig.base.json", + "references": [ + { + "path": "./tsconfig.src.json" + } + ] +} diff --git a/packages/slack/tsconfig.src.json b/packages/slack/tsconfig.src.json new file mode 100644 index 00000000000..db06c53317e --- /dev/null +++ b/packages/slack/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src/**/*.ts"], + "compilerOptions": { + "isolatedDeclarations": false, + "composite": true, + "sourceMap": true, + "customConditions": ["@triggerdotdev/source"] + } +} diff --git a/packages/slack/vitest.config.ts b/packages/slack/vitest.config.ts new file mode 100644 index 00000000000..4afd9264256 --- /dev/null +++ b/packages/slack/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.test.ts"], + globals: true, + }, +}); diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index 4612302c4ae..23da6be9bb6 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -49,6 +49,16 @@ import type { JSONSchema7, Schema, } from "ai"; +import type { AnyChatEvent, ChatEventActions } from "@trigger.dev/core/v3"; +import type { + ValidatedWebhookKey, + ValidateWebhookFilter, + WebhookVerifierArtifact, + WebhookSecretProvisioning, + AnyWebhookSource, + InferWebhookEvent, +} from "@trigger.dev/core/v3"; +import { chatEvent, normalizeKeyString } from "./webhooks.js"; // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require` // ESM-only `ai@7` (see ../imports/ai-runtime.ts). import { type Attributes, trace } from "@opentelemetry/api"; @@ -170,6 +180,17 @@ const chatSessionHandleKey = locals.create("chat.sessionHandle"); // metadata reads this directly rather than the Session handle id. const chatExternalIdKey = locals.create("chat.externalId"); +/** + * Set at boot when a continuation run inherited an interrupted turn (a + * partial assistant reply on `session.out`). Consumed by the first channel + * turn's ack so the re-emitted placeholder reads as a recovery resume rather + * than a silent duplicate. Boxed so the ack site can flip it to false. + * @internal + */ +const chatChannelRecoveryPendingKey = locals.create<{ value: boolean }>( + "chat.channelRecoveryPending" +); + /** * S2 seq_num of the most recent `turn-complete` control record written by * this worker. Read by `writeTurnCompleteChunk` to know what to trim back @@ -1242,6 +1263,41 @@ function createChatAccessToken( return auth.createTriggerPublicToken(taskId as string, { expirationTime: "24h" }); } +/** + * Mint a read-only, session-scoped token for WATCHING a chat session by its `externalId`. + * + * A session is addressed by `externalId`, so the same session is reachable from any surface that + * knows it: a web `useChat` client and a Slack thread converge on one session when they share an + * `externalId` (for a channel, the connector's `key` template produces it). This token carries + * `read:sessions:{externalId}` only (no write): the holder can subscribe to the session's `.out` + * stream (observe the conversation, hydrating history from the snapshot) but cannot append to `.in`. + * + * Use it for a dashboard viewer or a cross-surface watcher of a Slack-thread session. Run server-side + * (needs the secret key). To CONTINUE a session from another surface (drive it, not just watch), mint + * a read+write token instead ({@link createChatStartSessionAction}). + * + * @example + * ```ts + * // actions.ts + * "use server"; + * import { chat } from "@trigger.dev/sdk/ai"; + * + * export const watchChat = (externalId: string) => chat.createWatchToken(externalId); + * ``` + */ +function createChatWatchToken( + externalId: string, + options?: { tokenTTL?: string } +): Promise { + if (!externalId) { + throw new Error("chat.createWatchToken: externalId is required (the session addressing key)."); + } + return auth.createPublicToken({ + scopes: { read: { sessions: externalId } }, + expirationTime: options?.tokenTTL ?? "1h", + }); +} + // --------------------------------------------------------------------------- // Chat transport helpers — backend side // --------------------------------------------------------------------------- @@ -1531,6 +1587,11 @@ export type ChatTaskRunPayload< * `tools` option on `chat.agent`). Empty object when no `tools` were declared. */ tools: TTools; + /** + * Present only for a channel-delivered turn whose connector supports reactions. Lets the agent + * react to the triggering message to signal meaning (e.g. `channel.react("white_check_mark")`). + */ + channel?: ChannelRunSurface; }; // Input streams for bidirectional chat communication @@ -4641,12 +4702,434 @@ export type ChatResumeEvent = { + event: TEvent; + deliveryId: string; + previousRef?: string; + mode: "final" | "stream"; + final: boolean; +}; + +export type ChannelAckCtx = { + /** + * True when this ack is being re-posted because the run is recovering an + * interrupted turn after a crash/continuation. The prior run's message ref + * is gone, so egress re-emits into the thread as a fresh message; a + * connector can vary the placeholder text to read as a deliberate resume + * ("picking this back up...") rather than a silent duplicate. + */ + recovered: boolean; +}; + +/** + * A tool call the turn paused on, awaiting a human decision (HITL). `renderInteraction` maps these to + * the controls posted in the thread; the callback resolves one by `toolCallId`. + */ +export type ChannelPendingToolCall = { toolCallId: string; toolName: string; input?: unknown }; + +/** + * A verified interaction callback resolved to a tool output. `onInteraction` returns this to resume the + * paused run: the framework stitches `output` onto the pending tool part (by `toolCallId`) and continues. + */ +export type ChannelInteractionResolution = { toolCallId: string; output: unknown }; + +export type ChannelInteractionCtx = { event: TEvent; deliveryId: string }; + +// A reaction to add (or remove) on the triggering message. `name` is a provider emoji id (e.g. "eyes"). +export type ChannelReaction = { name: string; remove?: boolean }; +export type ChannelReactCtx = { event: TEvent; deliveryId: string }; + +// A lifecycle reaction choice: one emoji name, an array (one picked at random per turn), or a resolver +// of the event returning either (null/undefined to skip). Names are provider emoji ids without colons. +export type ChannelReactionChoice = + | string + | string[] + | (( + event: TEvent + ) => string | string[] | null | undefined | Promise); + +// Lifecycle reactions the run loop applies to the user's message around a turn (add working at start, +// swap to done at complete, error on failure). Any subset; requires the connector's `react`. +export type ChannelReactions = { + working?: ChannelReactionChoice; + done?: ChannelReactionChoice; + error?: ChannelReactionChoice; +}; + +// Handed to run() for a channel-delivered turn (when the connector supports reactions), so the agent can +// react to the triggering message to signal meaning. Best-effort: a failed reaction never throws. +export type ChannelRunSurface = { + react: (name: string) => Promise; + unreact: (name: string) => Promise; +}; + +// Reserved for 2c: resolve a provider credential keyed by the incoming event's installation (team_id), +// not the connector (one connector serves many Slack workspaces). The in-run send() calls it at post time. +export type ResolveChannelToken = (event: TEvent) => Promise; + +declare const channelEventPhantom: unique symbol; + +export interface ChannelConnector { + id: string; + source: string; + key: string; // compiled canonical key template + verifierArtifact: WebhookVerifierArtifact; + secretProvisioning?: WebhookSecretProvisioning; + filter?: string; + // Gate session creation: a resolved key with no session is only started when the event matches this + // filter (existing sessions always resume). Absent => every routed event can start one. + startOn?: string; + inbound: (event: TEvent) => ChannelMessageInput; + // Egress (optional; a channel can be inbound-only). Fires only when `send` is set. + outbound?: (reply: ChannelReply) => ChannelMessage | null; + ack?: (event: TEvent, ctx: ChannelAckCtx) => ChannelMessage | null; // placeholder posted at turn start ("final" mode) + send?: (message: ChannelMessage, ctx: ChannelSendCtx) => Promise<{ ref?: string }>; + /** HITL: controls posted when a turn pauses on a human-decision tool (a tool with no `execute`). */ + renderInteraction?: ( + pending: ChannelPendingToolCall[], + ctx: ChannelInteractionCtx + ) => ChannelMessage | null; + /** + * HITL: map a verified callback event to a tool resolution. Non-null resumes the paused run; null + * means treat the event as a normal inbound message (a new turn). + */ + onInteraction?: (event: TEvent) => ChannelInteractionResolution | null; + /** + * HITL: finalize the posted controls once a decision is made (edit them away, show the outcome), so + * the buttons can't be clicked again. Called with the same verified callback event when + * `onInteraction` resolves, before the run resumes. Best-effort: a throw is logged and the run continues. + */ + finalizeInteraction?: ( + event: TEvent, + resolution: ChannelInteractionResolution + ) => Promise | void; + delivery: "final" | "stream"; // "final" (ack + edit) is v1; "stream" (debounced edits) is a fast-follow + // Add/remove a reaction on the triggering message. Powers lifecycle reactions + run()'s channel surface. + react?: (reaction: ChannelReaction, ctx: ChannelReactCtx) => Promise; + reactions?: ChannelReactions; + readonly [channelEventPhantom]?: TEvent; +} +export type AnyChannelConnector = ChannelConnector; + +/** + * A generic chat-frontend channel over any verified source. Unlike `slack()`, you supply the egress + * `send` yourself (post/edit the reply back to your surface), so the whole round-trip is under your + * control. Use for providers without a preset, or to test the channel round-trip end to end. + */ +export function chatChannelCustom< + TSource extends AnyWebhookSource, + const TKey extends string = string, + const TFilter extends string = string, + const TStartOn extends string = string, +>(options: { + id: string; + source: TSource; + key: ValidatedWebhookKey, TKey>; + inbound: (event: InferWebhookEvent) => ChannelMessageInput; + outbound?: (reply: ChannelReply) => ChannelMessage | null; + ack?: (event: InferWebhookEvent, ctx: ChannelAckCtx) => ChannelMessage | null; + send?: ( + message: ChannelMessage, + ctx: ChannelSendCtx> + ) => Promise<{ ref?: string }>; + renderInteraction?: ( + pending: ChannelPendingToolCall[], + ctx: ChannelInteractionCtx> + ) => ChannelMessage | null; + onInteraction?: (event: InferWebhookEvent) => ChannelInteractionResolution | null; + finalizeInteraction?: ( + event: InferWebhookEvent, + resolution: ChannelInteractionResolution + ) => Promise | void; + react?: ( + reaction: ChannelReaction, + ctx: ChannelReactCtx> + ) => Promise; + reactions?: ChannelReactions>; + filter?: TFilter & ValidateWebhookFilter, TFilter>; + // Only start a new session when the event matches this filter (existing sessions always resume). + startOn?: TStartOn & ValidateWebhookFilter, TStartOn>; + delivery?: "final" | "stream"; +}): ChannelConnector> { + const { + id, + source, + key, + inbound, + outbound, + ack, + send, + renderInteraction, + onInteraction, + finalizeInteraction, + react, + reactions, + filter, + startOn, + delivery, + } = options; + resourceCatalog.registerDeclaredSessionWebhook(id); + return { + id, + source: source.provider, + key: normalizeKeyString(key as string), + verifierArtifact: source.verifier, + secretProvisioning: source.secretProvisioning, + filter, + startOn, + inbound, + outbound, + ack, + send, + renderInteraction, + onInteraction, + finalizeInteraction, + react, + reactions, + delivery: delivery ?? "final", + } as ChannelConnector>; +} + +const chatChannels = { + /** A generic chat-frontend channel over any source, with your own egress. See {@link chatChannelCustom}. */ + custom: chatChannelCustom, +}; + +// Turn a channel connector's inbound() result into a user UIMessage for the turn. +function toUserUIMessage(input: ChannelMessageInput, messageId: string): UIMessage { + if (typeof input !== "string") return input; + return { id: messageId, role: "user", parts: [{ type: "text", text: input }] } as UIMessage; +} + +/** + * The assistant's closing text for a channel reply: the text after the last tool part in the message, + * so a pre-tool preamble (e.g. the HITL "I'll need approval first" line, or a "let me look that up") + * is dropped and only the answer is posted. Falls back to all text when there's no trailing text or no + * tool parts (an ordinary turn), so a plain reply is unchanged. + */ +function channelReplyText(message: UIMessage): string { + const parts = (message.parts ?? []) as any[]; + const isTool = (p: any) => { + const t = p?.type; + return typeof t === "string" && (t.startsWith("tool-") || t === "dynamic-tool"); + }; + let lastToolIdx = -1; + parts.forEach((p, i) => { + if (isTool(p)) lastToolIdx = i; + }); + const textFrom = (from: number) => + parts + .slice(from) + .map((p) => + p && typeof p === "object" && "text" in p ? String((p as { text: unknown }).text) : "" + ) + .join(""); + const trailing = textFrom(lastToolIdx + 1); + return trailing.trim().length > 0 ? trailing : textFrom(0); +} + +/** Tool parts of a UIMessage awaiting a human answer (`input-available`), for `renderInteraction`. */ +function pendingToolCallsInMessage(message: UIMessage): ChannelPendingToolCall[] { + const out: ChannelPendingToolCall[] = []; + for (const part of (message.parts ?? []) as any[]) { + if (!part || typeof part !== "object") continue; + const type = part.type; + const isTool = + typeof type === "string" && (type.startsWith("tool-") || type === "dynamic-tool"); + if (!isTool || part.state !== "input-available") continue; + const toolName = + type === "dynamic-tool" ? String(part.toolName ?? "") : String(type).slice("tool-".length); + if (typeof part.toolCallId === "string") + out.push({ toolCallId: part.toolCallId, toolName, input: part.input }); + } + return out; +} + +/** + * Build the slim assistant message the HITL resume path expects (mirrors `slimSubmitMessageForWire`): + * the pending tool part matched by `toolCallId` across `messages`, advanced to `output-available` with + * the interaction's output. Returns undefined when no pending part matches (a stale/duplicate callback). + */ +function buildInteractionResolutionMessage( + resolution: ChannelInteractionResolution, + messages: UIMessage[] +): UIMessage | undefined { + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]!; + if (message.role !== "assistant") continue; + for (const part of (message.parts ?? []) as any[]) { + if (!part || typeof part !== "object" || part.toolCallId !== resolution.toolCallId) continue; + const type = part.type; + const isTool = + typeof type === "string" && (type.startsWith("tool-") || type === "dynamic-tool"); + if (!isTool) continue; + const slimPart: Record = { + type, + toolCallId: resolution.toolCallId, + state: "output-available", + output: resolution.output, + }; + if (type === "dynamic-tool" && typeof part.toolName === "string") + slimPart.toolName = part.toolName; + return { id: message.id, role: "assistant", parts: [slimPart] } as unknown as UIMessage; + } + } + return undefined; +} + +// Default outbound: post the reply text, or nothing when empty (a tool-only / empty turn). +function defaultChannelOutbound(reply: ChannelReply): ChannelMessage | null { + return reply.text ? { text: reply.text } : null; +} + +// Resolve a lifecycle reaction choice to one emoji name for this turn: call a resolver if given, then +// pick one at random from an array. Returns undefined to skip (no choice / empty). +export async function resolveReactionChoice( + choice: ChannelReactionChoice | undefined, + event: unknown +): Promise { + if (choice == null) return undefined; + let value: string | string[] | null | undefined = + typeof choice === "function" ? await choice(event) : choice; + if (Array.isArray(value)) { + if (value.length === 0) return undefined; + value = value[Math.floor(Math.random() * value.length)]; + } + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +// Best-effort reaction on the triggering message; a failure never breaks the turn. +async function applyChannelReaction( + connector: AnyChannelConnector | undefined, + wireEvent: { event: unknown; deliveryId: string } | undefined, + reaction: ChannelReaction +): Promise { + if (!connector?.react || !wireEvent || !reaction.name) return; + try { + await connector.react(reaction, { event: wireEvent.event, deliveryId: wireEvent.deliveryId }); + } catch (error) { + logger.warn("chat.agent: channel reaction failed", { error, name: reaction.name }); + } +} + +// The run()-facing reaction surface for a channel turn (flavor 2). Undefined when the connector has no +// `react`, so `payload.channel` is only present when reacting is actually possible. +function buildChannelRunSurface( + connector: AnyChannelConnector | undefined, + wireEvent: { event: unknown; deliveryId: string } | undefined +): ChannelRunSurface | undefined { + if (!connector?.react || !wireEvent) return undefined; + return { + react: (name) => applyChannelReaction(connector, wireEvent, { name }), + unreact: (name) => applyChannelReaction(connector, wireEvent, { name, remove: true }), + }; +} + +// "stream" egress: as the reply streams, debounce-edit the ack message (previousRef) with the growing +// text. Trailing-edge, one edit per interval (Slack chat.update ~1/s); the turn-complete final edit is +// the authoritative last write. Best-effort: an edit failure is logged, never fatal. +const CHANNEL_STREAM_EDIT_INTERVAL_MS = 1000; +function makeChannelStreamEditor( + connector: ChannelConnector, + channelEvent: { event: unknown; deliveryId: string }, + ackRef: string +) { + const outbound = connector.outbound ?? defaultChannelOutbound; + let latest = ""; + let timer: ReturnType | undefined; + let inFlight = false; + let stopped = false; + + const edit = async () => { + if (stopped || inFlight) return; + const text = latest; + const message = outbound({ + text, + message: { id: ackRef, role: "assistant", parts: [{ type: "text", text }] } as UIMessage, + final: false, + stopped: false, + }); + if (!message) return; + inFlight = true; + try { + await connector.send!(message, { + event: channelEvent.event as TEvent, + deliveryId: channelEvent.deliveryId, + previousRef: ackRef, + mode: "stream", + final: false, + }); + } catch (error) { + logger.warn("chat.agent: channel stream edit failed", { error }); + } finally { + inFlight = false; + } + }; + + return { + observe(chunk: unknown) { + if (stopped) return; + const c = chunk as { type?: string; delta?: unknown }; + if (c?.type === "text-delta" && typeof c.delta === "string") { + latest += c.delta; + if (!timer) + timer = setTimeout(() => { + timer = undefined; + void edit(); + }, CHANNEL_STREAM_EDIT_INTERVAL_MS); + } + }, + stop() { + stopped = true; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + }, + }; +} + +// The `action` type onAction receives: the actionSchema output (when set) unioned with the action +// envelopes of any listed chat.event descriptors. ChatEventActions<[]> is `never`, so it collapses +// cleanly when no events are listed; `unknown` is kept only when neither source is present. +type ChatActionType< + TActionSchema extends TaskSchema | undefined, + TW extends readonly AnyChatEvent[], +> = [TActionSchema] extends [TaskSchema] + ? inferSchemaOut | ChatEventActions + : [TW] extends [readonly []] + ? unknown + : ChatEventActions; + export type ChatAgentOptions< TIdentifier extends string, TClientDataSchema extends TaskSchema | undefined = undefined, TUIMessage extends UIMessage = UIMessage, TActionSchema extends TaskSchema | undefined = undefined, TTools extends ToolSet = ToolSet, + TW extends readonly AnyChatEvent[] = [], + TChannels extends readonly AnyChannelConnector[] = [], > = Omit< TaskOptions< TIdentifier, @@ -4749,9 +5232,25 @@ export type ChatAgentOptions< * `StreamTextResult` (auto-piped), `string`, or `UIMessage`. Returning * `void` or nothing is the side-effect-only default. */ + /** + * Inbound `chat.event(...)` descriptors this agent handles. Listing one registers an agent-scoped + * webhook endpoint that routes verified deliveries to this agent's session; the delivery arrives at + * `onAction` as a `{ type, event, source, headers, deliveryId }` envelope whose `type` is the + * descriptor's `type`. The same descriptor listed on another agent gets its own endpoint. + */ + events?: TW; + + /** + * Inbound chat frontends (Slack, etc.) via `chat.channels.*`. Listing one registers an agent-scoped + * webhook endpoint that routes verified events to a durable per-key session and delivers them as + * turns: the connector's `inbound()` maps the raw event to the turn's message, `run()` fires as + * normal, and the reply streams back to the channel. Use the connector's `filter` to ignore events. + */ + channels?: TChannels; + onAction?: ( event: ActionEvent< - [TActionSchema] extends [TaskSchema] ? inferSchemaOut : unknown, + ChatActionType, inferSchemaOut, TUIMessage > @@ -5394,8 +5893,18 @@ function chatAgent< TUIMessage extends UIMessage = UIMessage, TActionSchema extends TaskSchema | undefined = undefined, TTools extends ToolSet = ToolSet, + const TW extends readonly AnyChatEvent[] = [], + const TChannels extends readonly AnyChannelConnector[] = [], >( - options: ChatAgentOptions + options: ChatAgentOptions< + TIdentifier, + TClientDataSchema, + TUIMessage, + TActionSchema, + TTools, + TW, + TChannels + > ): Task>, unknown> { const { run: userRun, @@ -5407,6 +5916,8 @@ function chatAgent< onValidateMessages, hydrateMessages, actionSchema, + events, + channels, onAction, onTurnStart, onBeforeTurnComplete, @@ -5780,6 +6291,10 @@ function chatAgent< // those route through the normal continuation-wait path. const hasRecoveredState = partialAssistant !== undefined; + if (couldHavePriorState && hasRecoveredState) { + locals.set(chatChannelRecoveryPendingKey, { value: true }); + } + let hookChain: TUIMessage[] | undefined; let hookRecoveredTurns: TUIMessage[] | undefined; let hookBeforeBoot: (() => Promise) | undefined; @@ -6389,6 +6904,10 @@ function chatAgent< // Declared here so the finally can detach it — a handler leaked past // its turn duplicates every mid-stream message into the shared buffer. let turnMsgSub: { off: () => void } | undefined; + // Hoisted so the catch can run the error-reaction lifecycle; assigned inside the try. + let channelConn: AnyChannelConnector | undefined; + let channelWorkingReaction: string | undefined; + let channelWireEvent: { event: unknown; deliveryId: string } | undefined; try { // Extract turn-level context before entering the span. Slim // wire: at most one delta message per record. `headStartMessages` @@ -6398,11 +6917,74 @@ function chatAgent< metadata: wireMetadata, message: incomingMessage, headStartMessages: _hsm, + channelEvent: wireChannelEvent, ...restWire } = currentWirePayload; void _hsm; - const incomingMessages: TUIMessage[] = incomingMessage - ? [incomingMessage as TUIMessage] + channelWireEvent = wireChannelEvent; + let effectiveIncomingMessage = incomingMessage; + let channelAckRef: string | undefined; + if (wireChannelEvent) { + channelConn = channels?.find((c) => c.id === wireChannelEvent.connectorId); + if (channelConn) { + const interaction = channelConn.onInteraction?.(wireChannelEvent.event) ?? null; + const resolutionMessage = interaction + ? buildInteractionResolutionMessage( + interaction, + accumulatedUIMessages as UIMessage[] + ) + : undefined; + if (resolutionMessage) { + effectiveIncomingMessage = resolutionMessage as typeof incomingMessage; + if (interaction && channelConn.finalizeInteraction) { + try { + await channelConn.finalizeInteraction(wireChannelEvent.event, interaction); + } catch (finalizeError) { + logger.warn("chat.agent: channel finalizeInteraction failed; continuing", { + error: finalizeError, + }); + } + } + } else { + effectiveIncomingMessage = toUserUIMessage( + channelConn.inbound(wireChannelEvent.event), + currentWirePayload.messageId ?? wireChannelEvent.deliveryId + ) as typeof incomingMessage; + if (channelConn.send && channelConn.ack) { + const recoveryPending = locals.get(chatChannelRecoveryPendingKey); + const recovered = recoveryPending?.value === true; + if (recovered) recoveryPending!.value = false; + const ackMessage = channelConn.ack(wireChannelEvent.event, { recovered }); + if (ackMessage) { + try { + const ackResult = await channelConn.send(ackMessage, { + event: wireChannelEvent.event, + deliveryId: wireChannelEvent.deliveryId, + mode: channelConn.delivery, + final: false, + }); + channelAckRef = ackResult?.ref; + } catch (ackError) { + logger.warn("chat.agent: channel ack post failed; continuing", { + error: ackError, + }); + } + } + } + channelWorkingReaction = await resolveReactionChoice( + channelConn.reactions?.working, + wireChannelEvent.event + ); + if (channelWorkingReaction) { + await applyChannelReaction(channelConn, wireChannelEvent, { + name: channelWorkingReaction, + }); + } + } + } + } + const incomingMessages: TUIMessage[] = effectiveIncomingMessage + ? [effectiveIncomingMessage as TUIMessage] : []; // Cleaning happens once here so `extractLastUserMessageText` and // every downstream consumer see the same message shape — and @@ -6561,9 +7143,11 @@ function chatAgent< let actionStreamResult: unknown = undefined; if (isAction) { // Parse and validate the action payload - const parsedAction = parseAction - ? await parseAction(currentWirePayload.action) - : currentWirePayload.action; + const isWebhookAction = currentWirePayload.actionSource === "webhook"; + const parsedAction = + parseAction && !isWebhookAction + ? await parseAction(currentWirePayload.action) + : currentWirePayload.action; // Hydrate messages from backend if configured if (hydrateMessages) { @@ -7138,6 +7722,7 @@ function chatAgent< signal: combinedSignal, cancelSignal, stopSignal, + channel: buildChannelRunSurface(channelConn, wireChannelEvent), } as any); } @@ -7179,7 +7764,32 @@ function chatAgent< resolveOnFinish!(); }, }); - await pipeChat(uiStream, { + let streamForPipe: typeof uiStream = uiStream; + if ( + wireChannelEvent && + channelConn?.send && + channelConn.delivery === "stream" && + channelAckRef && + uiStream instanceof ReadableStream + ) { + const editor = makeChannelStreamEditor( + channelConn, + wireChannelEvent, + channelAckRef + ); + streamForPipe = uiStream.pipeThrough( + new TransformStream({ + transform(chunk, controller) { + editor.observe(chunk); + controller.enqueue(chunk); + }, + flush() { + editor.stop(); + }, + }) + ); + } + await pipeChat(streamForPipe, { signal: combinedSignal, spanName: "stream response", }); @@ -7623,6 +8233,61 @@ function chatAgent< turnAccessToken ); + // Channel egress ("final"): map the turn's reply via outbound() and send it, editing + // the ack placeholder posted at turn start (previousRef). Best-effort: an egress failure + // is logged, not fatal. A manual-pipe turn has no responseMessage, so it opts out. + if (wireChannelEvent && channelConn?.send && turnCompleteEvent.responseMessage) { + const pendingToolCalls = pendingToolCallsInMessage( + turnCompleteEvent.responseMessage + ); + const channelMessage = + pendingToolCalls.length > 0 && channelConn.renderInteraction + ? channelConn.renderInteraction(pendingToolCalls, { + event: wireChannelEvent.event, + deliveryId: wireChannelEvent.deliveryId, + }) + : (channelConn.outbound ?? defaultChannelOutbound)({ + text: channelReplyText(turnCompleteEvent.responseMessage), + message: turnCompleteEvent.responseMessage, + final: true, + stopped: turnCompleteEvent.stopped, + }); + if (channelMessage) { + try { + await channelConn.send(channelMessage, { + event: wireChannelEvent.event, + deliveryId: wireChannelEvent.deliveryId, + previousRef: channelAckRef, + mode: channelConn.delivery, + final: true, + }); + } catch (egressError) { + logger.warn("chat.agent: channel egress send failed", { + error: egressError, + }); + } + } + } + + // Lifecycle reaction: turn done. Remove "working", mark "done". + if (wireChannelEvent && channelConn?.react) { + if (channelWorkingReaction) { + await applyChannelReaction(channelConn, wireChannelEvent, { + name: channelWorkingReaction, + remove: true, + }); + } + const doneReaction = await resolveReactionChoice( + channelConn.reactions?.done, + wireChannelEvent.event + ); + if (doneReaction) { + await applyChannelReaction(channelConn, wireChannelEvent, { + name: doneReaction, + }); + } + } + // Fire onTurnComplete — stream is closed, use for persistence. if (onTurnComplete) { await tracer.startActiveSpan( @@ -7878,6 +8543,22 @@ function chatAgent< throw turnError; } + if (channelWireEvent && channelConn?.react) { + if (channelWorkingReaction) { + await applyChannelReaction(channelConn, channelWireEvent, { + name: channelWorkingReaction, + remove: true, + }); + } + const errorReaction = await resolveReactionChoice( + channelConn.reactions?.error, + channelWireEvent.event + ); + if (errorReaction) { + await applyChannelReaction(channelConn, channelWireEvent, { name: errorReaction }); + } + } + let errorTurnCompleteResult: | Awaited> | undefined; @@ -8042,6 +8723,52 @@ function chatAgent< }); } + // Claim each listed chat.event descriptor: register an agent-scoped webhook endpoint that routes + // verified deliveries to THIS agent's session. The id is scoped by agent so the same descriptor on + // two agents yields two endpoints (no collision), each routing to its own agent. + if (events) { + for (const wh of events) { + resourceCatalog.markSessionWebhookClaimed(wh.id); + resourceCatalog.registerWebhookMetadata({ + id: `${options.id}:${wh.id}`, + source: wh.source, + verifierArtifact: wh.verifierArtifact, + secretProvisioning: wh.secretProvisioning, + filter: wh.filter, + routingTarget: { + type: "session", + taskIdentifier: options.id, + keyTemplate: wh.key, + actionType: wh.type, + deliverAs: "action", + }, + }); + } + } + + // Claim each listed channel connector: same agent-scoped endpoint, but deliverAs "message" so a + // verified event becomes a turn (the run maps it via the connector's inbound(), resolved by connectorId). + if (channels) { + for (const ch of channels) { + resourceCatalog.markSessionWebhookClaimed(ch.id); + resourceCatalog.registerWebhookMetadata({ + id: `${options.id}:${ch.id}`, + source: ch.source, + verifierArtifact: ch.verifierArtifact, + secretProvisioning: ch.secretProvisioning, + filter: ch.filter, + routingTarget: { + type: "session", + taskIdentifier: options.id, + keyTemplate: ch.key, + connectorId: ch.id, + deliverAs: "message", + startOn: ch.startOn, + }, + }); + } + } + return task; } @@ -10541,6 +11268,10 @@ async function mintPublicTokenWithOverride(args: { export const chat = { /** Create a chat agent. See {@link chatAgent}. */ agent: chatAgent, + /** Declare an inbound webhook event an agent claims via `chat.agent({ events })`. See {@link chatEvent}. */ + event: chatEvent, + /** Chat frontend connectors (Slack, etc.) an agent claims via `chat.agent({ channels })`. */ + channels: chatChannels, /** Create a custom agent with manual lifecycle control. See {@link chatCustomAgent}. */ customAgent: chatCustomAgent, /** Create a chat task with a fixed {@link UIMessage} subtype and optional default stream options. See {@link withUIMessage}. */ @@ -10555,6 +11286,8 @@ export const chat = { local: chatLocal, /** Create a public access token for a chat task. See {@link createChatAccessToken}. */ createAccessToken: createChatAccessToken, + /** Mint a read-only token to WATCH a session by externalId (cross-surface). See {@link createChatWatchToken}. */ + createWatchToken: createChatWatchToken, /** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */ setTurnTimeout, /** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */ diff --git a/packages/trigger-sdk/src/v3/channelReactions.test.ts b/packages/trigger-sdk/src/v3/channelReactions.test.ts new file mode 100644 index 00000000000..f9d806d0e05 --- /dev/null +++ b/packages/trigger-sdk/src/v3/channelReactions.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { resolveReactionChoice } from "./ai.js"; + +describe("resolveReactionChoice", () => { + it("returns a single string as-is", async () => { + expect(await resolveReactionChoice("eyes", {})).toBe("eyes"); + }); + + it("skips when absent or empty", async () => { + expect(await resolveReactionChoice(undefined, {})).toBeUndefined(); + expect(await resolveReactionChoice("", {})).toBeUndefined(); + expect(await resolveReactionChoice([], {})).toBeUndefined(); + expect(await resolveReactionChoice(() => undefined, {})).toBeUndefined(); + expect(await resolveReactionChoice(() => null, {})).toBeUndefined(); + }); + + it("picks a member of an array (randomly)", async () => { + const options = ["eyes", "hourglass", "thinking_face"]; + const seen = new Set(); + for (let i = 0; i < 60; i++) { + const picked = await resolveReactionChoice(options, {}); + expect(options).toContain(picked); + seen.add(picked!); + } + // Over 60 draws from 3 options, seeing only one is astronomically unlikely: proves it varies. + expect(seen.size).toBeGreaterThan(1); + }); + + it("resolves a function of the event, returning a string or an array", async () => { + const byKind = (e: unknown) => `emoji-${(e as { kind: string }).kind}`; + expect(await resolveReactionChoice(byKind, { kind: "bug" })).toBe("emoji-bug"); + const picked = await resolveReactionChoice(() => ["a", "b"], {}); + expect(["a", "b"]).toContain(picked); + }); + + it("awaits an async resolver", async () => { + expect(await resolveReactionChoice(async () => "shipit", {})).toBe("shipit"); + }); +}); diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts index 9dddac1c0eb..6979a0f8165 100644 --- a/packages/trigger-sdk/src/v3/chat.ts +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -97,6 +97,24 @@ export type ChatTaskWirePayload a chat frontend like Slack). Carries the raw + * verified provider event; the run resolves the connector by `connectorId` from `chat.agent({ channels })` + * and applies its `inbound()` mapper to produce the turn's message. Present instead of `message`. + */ + channelEvent?: { + connectorId: string; + event: unknown; + source: string; + headers: Record; + deliveryId: string; + }; /** Whether this run is continuing an existing chat whose previous run ended. */ continuation?: boolean; /** The run ID of the previous run (only set when `continuation` is true). */ diff --git a/packages/trigger-sdk/src/v3/webhooks.ts b/packages/trigger-sdk/src/v3/webhooks.ts index 040b7ee6638..cdbce6457fa 100644 --- a/packages/trigger-sdk/src/v3/webhooks.ts +++ b/packages/trigger-sdk/src/v3/webhooks.ts @@ -1,5 +1,32 @@ import { Webhook } from "@trigger.dev/core/v3"; import { subtle } from "../imports/uncrypto.js"; +import { createTask } from "./shared.js"; +import { resourceCatalog } from "@trigger.dev/core/v3"; +import type { + WebhookSource, + InferWebhookEvent, + AnyWebhookSource, + WebhookRunPayload, + ValidateWebhookFilter, + ChatEvent, + ValidatedWebhookKey, +} from "@trigger.dev/core/v3"; +import type { + WebhookVerifierConfig, + StripeWebhookEvent, + GitHubWebhookEvent, +} from "@trigger.dev/core/v3"; +import { + discordVerifierConfig, + githubVerifierConfig, + squareVerifierConfig, + stripeVerifierConfig, + svixVerifierConfig, + webhookProviderConfigs, + type WebhookProviderId, +} from "@trigger.dev/core/webhooks"; +import type { Task } from "./shared.js"; +import type { TaskRunContext } from "@trigger.dev/core/v3"; /** * The type of error thrown when a webhook fails to parse or verify @@ -24,6 +51,275 @@ type ConstructEventOptions = { header: string | Buffer | Array; }; +// ── Source producers (presets carry the event type) ── +export const webhookSources = { + custom(config: WebhookVerifierConfig): WebhookSource { + // Roll-your-own webhooks: you control both ends, so offer paste AND generate. + return { + provider: "custom", + verifier: { kind: "config", config }, + secretProvisioning: "either", + }; + }, + + // Stripe: `Stripe-Signature: t=…,v1=…` (comma-kv), signed `{t}.{body}`, hex. + // Defaults to a minimal event shape; pass the official type for full typing: stripe(). + stripe(opts?: { toleranceSeconds?: number }): WebhookSource { + return { + provider: "stripe", + verifier: { kind: "preset", preset: "stripe", config: stripeVerifierConfig(opts) }, + secretProvisioning: "provider", + }; + }, + + // GitHub: `X-Hub-Signature-256: sha256=` (prefixed), signed raw body. + // Defaults to an open shape; pass your event type for full typing: github(). + github(): WebhookSource { + return { + provider: "github", + verifier: { kind: "preset", preset: "github", config: githubVerifierConfig() }, + secretProvisioning: "integrator", + }; + }, + + // Svix family (Svix, Clerk, Resend): `svix-signature: v1, v1,` (space-list), + // signed `{id}.{timestamp}.{body}`, base64; the `whsec_` secret is base64-decoded. + svix(): WebhookSource { + return { + provider: "svix", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + // Square: bare base64 signature over `{notificationURL}{body}` (URL template var, no separator). + square(): WebhookSource { + return { + provider: "square", + verifier: { kind: "preset", preset: "square", config: squareVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + // Discord: asymmetric Ed25519 over `{timestamp}{body}`. The "secret" stored on the endpoint is + // the application PUBLIC KEY (hex by default). No shared secret. + discord(opts: { publicKeyEncoding?: "raw-hex" | "pem" } = {}): WebhookSource { + return { + provider: "discord", + verifier: { kind: "preset", preset: "discord", config: discordVerifierConfig(opts) }, + secretProvisioning: "provider", + }; + }, + + /** + * Per-provider producers over shared presets. Each is a thin wrapper: same verifier config as the + * preset it references, differing only in `provider` (routing + picker identity) and who provisions + * the secret. Pass the provider's own published type for full typing, e.g. clerk(). + */ + clerk(): WebhookSource { + return { + provider: "clerk", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + resend(): WebhookSource { + return { + provider: "resend", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + openai(): WebhookSource { + return { + provider: "openai", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + replicate(): WebhookSource { + return { + provider: "replicate", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + recallai(): WebhookSource { + return { + provider: "recall-ai", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + brex(): WebhookSource { + return { + provider: "brex", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "provider", + }; + }, + + gitlab(): WebhookSource { + return { + provider: "gitlab", + verifier: { kind: "preset", preset: "svix", config: svixVerifierConfig() }, + secretProvisioning: "integrator", + }; + }, + + whatsapp(): WebhookSource { + return { + provider: "whatsapp", + verifier: { kind: "preset", preset: "github", config: githubVerifierConfig() }, + secretProvisioning: "integrator", + }; + }, +} as const; + +/** + * Per-provider producers generated from the core config table (kind "config"). Each carries the + * provider's own HMAC verifier config and stays in lockstep with the round-trip-tested configs. + */ +export type ProviderProducers = { + [K in WebhookProviderId]: () => WebhookSource; +}; + +export const providerProducers = Object.fromEntries( + Object.entries(webhookProviderConfigs).map(([provider, entry]) => [ + provider, + () => ({ + provider, + verifier: { kind: "config" as const, config: entry.config() }, + secretProvisioning: entry.secretProvisioning, + }), + ]) +) as ProviderProducers; + +// ── webhook() entry: single-callback IoC, infers event from source ── +export type WebhookOnEventParams = { + /** The verified event body, typed by the source (a preset type, or the `` you supply). */ + event: TEvent; + /** The inbound request headers (case-insensitive, Web `Headers`). e.g. headers.get("x-github-event"). */ + headers: Headers; + ctx: TaskRunContext; +}; + +export type WebhookOptions = { + id: TIdentifier; + source: TSource; + /** + * Optional server-side filter (a type-safe string DSL checked against the event shape). A delivery + * that doesn't match is received and recorded but not routed (no run). e.g. + * `"event.action == 'created' && event.repository.private == false"`. + */ + filter?: string; + onEvent: (params: WebhookOnEventParams>) => Promise | void; +}; + +export type WebhookHandle = Task; + +export function webhook< + TIdentifier extends string, + TSource extends AnyWebhookSource, + const TFilter extends string = string, +>( + options: WebhookOptions & { + filter?: TFilter & ValidateWebhookFilter, TFilter>; + } +): WebhookHandle> { + const { id, source, onEvent, filter } = options; + + // 1. The task half: webhook IS a first-class task kind (triggerSource "webhook"). + // The platform delivers a { event, headers } envelope; unwrap it for onEvent. The handle's + // payload type stays the event (webhook tasks are triggered by the ingress, not tasks.trigger). + const task = createTask, void>({ + id, + triggerSource: "webhook", + run: async (payload, runOptions) => { + const envelope = payload as unknown as WebhookRunPayload>; + await onEvent({ + event: envelope.event, + headers: new Headers(envelope.headers ?? {}), + ctx: runOptions.ctx, + }); + }, + }); + + // 2. The endpoint half: register the verifier + default routing target (this task) + filter. + resourceCatalog.registerWebhookMetadata({ + id, + source: source.provider, + verifierArtifact: source.verifier, + routingTarget: { type: "task", taskId: id }, + secretProvisioning: source.secretProvisioning, + filter, + }); + + return task; +} + +// ── chat.event(): declarative descriptor an agent claims via chat.agent({ events }). No handler. ── +// Carries the verifier (source), a validated string `key`, and a `type` discriminant. The `key` +// validates against the event/webhook/header namespaces and mirrors the stored {body.x} wire template. +export function chatEvent< + TSource extends AnyWebhookSource, + const TId extends string = string, + const TKey extends string = string, + const TType extends string = TId, + const TFilter extends string = string, +>(options: { + id: TId; + source: TSource; + key: ValidatedWebhookKey, TKey>; + /** The `action.type` the handler reads. Optional; defaults to `id`. */ + type?: TType; + /** Optional server-side filter (same type-safe DSL as `webhook()`); a non-match is recorded FILTERED and not routed. */ + filter?: TFilter & ValidateWebhookFilter, TFilter>; +}): ChatEvent> { + const { id, source, key, type, filter } = options; + const keyTemplate = normalizeKeyString(key as string); + + // Record the descriptor as declared so the indexer can flag it if no agent ever claims it. + resourceCatalog.registerDeclaredSessionWebhook(id); + + return { + id, + type: type ?? id, + key: keyTemplate, + source: source.provider, + verifierArtifact: source.verifier, + secretProvisioning: source.secretProvisioning, + filter, + } as ChatEvent>; +} + +// Public chat-event types (descriptor, the shared action union, and the key namespaces). +export type { + ChatEvent, + AnyChatEvent, + ChatEventAction, + ChatEventActions, + WebhookKeyMeta, +} from "@trigger.dev/core/v3"; + +// Brace placeholders without a recognized namespace default to the event body. webhook./header./body. +// pass through unchanged. +export function normalizeKeyString(key: string): string { + return key.replace(/\{([^}]+)\}/g, (_match, path: string) => + path.startsWith("webhook.") || path.startsWith("header.") || path.startsWith("body.") + ? `{${path}}` + : `{body.${path}}` + ); +} + +// P2 seam (TYPE only): +export type { CreateWebhookEndpointParams } from "@trigger.dev/core/v3"; + /** * Interface describing the webhook utilities */ @@ -50,14 +346,43 @@ interface Webhooks { /** Header name used for webhook signatures */ SIGNATURE_HEADER_NAME: string; + custom: typeof webhookSources.custom; + stripe: typeof webhookSources.stripe; + github: typeof webhookSources.github; + svix: typeof webhookSources.svix; + square: typeof webhookSources.square; + discord: typeof webhookSources.discord; + clerk: typeof webhookSources.clerk; + resend: typeof webhookSources.resend; + openai: typeof webhookSources.openai; + replicate: typeof webhookSources.replicate; + recallai: typeof webhookSources.recallai; + brex: typeof webhookSources.brex; + gitlab: typeof webhookSources.gitlab; + whatsapp: typeof webhookSources.whatsapp; } /** * Webhook utilities for handling incoming webhook requests */ -export const webhooks: Webhooks = { +export const webhooks: Webhooks & ProviderProducers = { + ...providerProducers, constructEvent, SIGNATURE_HEADER_NAME, + custom: webhookSources.custom, + stripe: webhookSources.stripe, + github: webhookSources.github, + svix: webhookSources.svix, + square: webhookSources.square, + discord: webhookSources.discord, + clerk: webhookSources.clerk, + resend: webhookSources.resend, + openai: webhookSources.openai, + replicate: webhookSources.replicate, + recallai: webhookSources.recallai, + brex: webhookSources.brex, + gitlab: webhookSources.gitlab, + whatsapp: webhookSources.whatsapp, }; async function constructEvent( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b09dc718a6f..da2dd01e221 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,6 +312,12 @@ importers: '@internal/tsql': specifier: workspace:* version: link:../../internal-packages/tsql + '@internal/webhook-engine': + specifier: workspace:* + version: link:../../internal-packages/webhook-engine + '@internal/webhook-sources': + specifier: workspace:* + version: link:../../internal-packages/webhook-sources '@internationalized/date': specifier: ^3.5.1 version: 3.5.1 @@ -1411,6 +1417,50 @@ importers: specifier: 0.5.0-alpha.4 version: 0.5.0-alpha.4 + internal-packages/webhook-engine: + dependencies: + '@internal/redis': + specifier: workspace:* + version: link:../redis + '@internal/tracing': + specifier: workspace:* + version: link:../tracing + '@trigger.dev/core': + specifier: workspace:* + version: link:../../packages/core + '@trigger.dev/database': + specifier: workspace:* + version: link:../database + '@trigger.dev/redis-worker': + specifier: workspace:* + version: link:../../packages/redis-worker + nanoid: + specifier: 3.3.8 + version: 3.3.8 + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@internal/testcontainers': + specifier: workspace:* + version: link:../testcontainers + rimraf: + specifier: 6.0.1 + version: 6.0.1 + + internal-packages/webhook-sources: + dependencies: + '@trigger.dev/core': + specifier: workspace:* + version: link:../../packages/core + zod: + specifier: 3.25.76 + version: 3.25.76 + devDependencies: + '@internal/webhook-engine': + specifier: workspace:* + version: link:../webhook-engine + packages/build: dependencies: '@prisma/config': @@ -2089,6 +2139,31 @@ importers: specifier: ^1.7.0 version: 1.7.0 + packages/slack: + dependencies: + '@trigger.dev/core': + specifier: workspace:4.5.0-rc.7 + version: link:../core + devDependencies: + '@arethetypeswrong/cli': + specifier: ^0.15.4 + version: 0.15.4 + '@trigger.dev/sdk': + specifier: workspace:4.5.0-rc.7 + version: link:../trigger-sdk + rimraf: + specifier: 6.0.1 + version: 6.0.1 + tshy: + specifier: ^3.0.2 + version: 3.0.2 + tsx: + specifier: 4.17.0 + version: 4.17.0 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) + packages/trigger-sdk: dependencies: '@ai-sdk/otel': @@ -2280,11 +2355,20 @@ packages: '@antfu/utils@9.3.0': resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} + '@arethetypeswrong/cli@0.15.4': + resolution: {integrity: sha512-YDbImAi1MGkouT7f2yAECpUMFhhA1J0EaXzIqoC5GGtK0xDgauLtcsZezm8tNq7d3wOFXH7OnY+IORYcG212rw==} + engines: {node: '>=18'} + hasBin: true + '@arethetypeswrong/cli@0.18.5': resolution: {integrity: sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==} engines: {node: '>=20'} hasBin: true + '@arethetypeswrong/core@0.15.1': + resolution: {integrity: sha512-FYp6GBAgsNz81BkfItRz8RLZO03w5+BaeiPma1uCfmxTnxbtuMrI/dbzGiOk8VghO108uFI0oJo0OkewdSHw7g==} + engines: {node: '>=18'} + '@arethetypeswrong/core@0.18.5': resolution: {integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==} engines: {node: '>=20'} @@ -8171,9 +8255,23 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.1.7': resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.4.2 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.1.7': resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: @@ -8185,18 +8283,33 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.7': resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.1.7': resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.1.7': resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.1.7': resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.7': resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} @@ -8834,6 +8947,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -8873,6 +8990,10 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cheminfo-types@1.8.1: resolution: {integrity: sha512-FRcpVkox+cRovffgqNdDFQ1eUav+i/Vq/CUd1hcfEl2bevntFlzznL+jE8g4twl6ElB7gZjCko6pYpXyMn+6dA==} @@ -9551,6 +9672,10 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -10493,10 +10618,6 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -11680,6 +11801,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lowercase-keys@1.0.1: resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} engines: {node: '>=0.10.0'} @@ -12295,11 +12419,6 @@ packages: react: 18.3.1 react-dom: 18.3.1 - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -12844,6 +12963,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -14609,14 +14732,26 @@ packages: tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.7: resolution: {integrity: sha512-ECqb8imSroX1UmUuhRBNPkkmtZ8mHEenieim80UVxG0M5wXVjY2Fp2tYXCPvk+nLy1geOhFpeD5YQhM/gF63Jg==} @@ -14699,6 +14834,9 @@ packages: typescript: optional: true + ts-expose-internals-conditionally@1.0.0-empty.0: + resolution: {integrity: sha512-F8m9NOF6ZhdOClDVdlM8gj3fDCav4ZIFSs/EI3ksQbAAXVSCN/Jh5OCJDDZWBuBy9psFc6jULGDlPwjMYMhJDw==} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -15201,6 +15339,11 @@ packages: engines: {node: '>=v14.16.0'} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.1.4: resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -15285,6 +15428,31 @@ packages: yaml: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': 24.13.3 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.1.7: resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -15724,6 +15892,16 @@ snapshots: '@antfu/utils@9.3.0': {} + '@arethetypeswrong/cli@0.15.4': + dependencies: + '@arethetypeswrong/core': 0.15.1 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + marked: 9.1.6 + marked-terminal: 7.1.0(marked@9.1.6) + semver: 7.8.1 + '@arethetypeswrong/cli@0.18.5': dependencies: '@arethetypeswrong/core': 0.18.5 @@ -15734,6 +15912,15 @@ snapshots: marked-terminal: 7.1.0(marked@9.1.6) semver: 7.8.1 + '@arethetypeswrong/core@0.15.1': + dependencies: + '@andrewbranch/untar.js': 1.0.3 + fflate: 0.8.3 + semver: 7.8.1 + ts-expose-internals-conditionally: 1.0.0-empty.0 + typescript: 6.0.3 + validate-npm-package-name: 5.0.0 + '@arethetypeswrong/core@0.18.5': dependencies: '@andrewbranch/untar.js': 1.0.3 @@ -17932,7 +18119,7 @@ snapshots: '@esbuild-kit/esm-loader@2.6.5': dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.7.6 + get-tsconfig: 4.14.0 '@esbuild/aix-ppc64@0.23.0': optional: true @@ -22947,6 +23134,13 @@ snapshots: tinyrainbow: 3.1.0 vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.7)(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0)) + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 @@ -22956,6 +23150,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@2.1.9(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) + '@vitest/mocker@4.1.7(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.7 @@ -22972,15 +23174,30 @@ snapshots: optionalDependencies: vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)(yaml@2.9.0) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@4.1.7': dependencies: tinyrainbow: 3.1.0 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@4.1.7': dependencies: '@vitest/utils': 4.1.7 pathe: 2.0.3 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/snapshot@4.1.7': dependencies: '@vitest/pretty-format': 4.1.7 @@ -22988,8 +23205,18 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@4.1.7': {} + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@4.1.7': dependencies: '@vitest/pretty-format': 4.1.7 @@ -23731,6 +23958,14 @@ snapshots: ccount@2.0.1: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chai@6.2.2: {} chalk@2.4.2: @@ -23760,6 +23995,8 @@ snapshots: chardet@0.7.0: {} + check-error@2.1.3: {} + cheminfo-types@1.8.1: {} chevrotain-allstar@0.4.1(chevrotain@12.0.0): @@ -24464,6 +24701,8 @@ snapshots: mimic-response: 3.1.0 optional: true + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deep-object-diff@1.1.9: {} @@ -25575,7 +25814,7 @@ snapshots: process-warning: 5.0.0 rfdc: 1.4.1 secure-json-parse: 4.0.0 - semver: 7.7.3 + semver: 7.8.1 toad-cache: 3.7.0 fastq@1.15.0: @@ -25686,11 +25925,6 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.1.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -26891,6 +27125,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lowercase-keys@1.0.1: {} lowercase-keys@2.0.0: {} @@ -27911,8 +28147,6 @@ snapshots: stacktrace-js: 2.0.2 stylis: 4.3.0 - nanoid@3.3.11: {} - nanoid@3.3.12: {} nanoid@3.3.8: {} @@ -28513,6 +28747,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + peberminta@0.9.0: {} peek-readable@5.4.2: {} @@ -28755,7 +28991,7 @@ snapshots: postcss@8.5.10: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -30602,10 +30838,16 @@ snapshots: '@types/tinycolor2': 1.4.3 tinycolor2: 1.6.0 + tinypool@1.1.1: {} + tinypool@2.1.0: {} + tinyrainbow@1.2.0: {} + tinyrainbow@3.1.0: {} + tinyspy@3.0.2: {} + tldts-core@7.0.7: {} tldts@7.0.7: @@ -30663,6 +30905,8 @@ snapshots: optionalDependencies: typescript: 6.0.3 + ts-expose-internals-conditionally@1.0.0-empty.0: {} + ts-interface-checker@0.1.13: {} ts-pattern@3.3.5: {} @@ -30710,8 +30954,8 @@ snapshots: dependencies: chalk: 5.3.0 chokidar: 3.6.0 - foreground-child: 3.1.1 - minimatch: 10.0.1 + foreground-child: 3.3.1 + minimatch: 10.2.5 mkdirp: 3.0.1 polite-json: 5.0.0 resolve-import: 2.0.0 @@ -30795,7 +31039,7 @@ snapshots: tsx@4.17.0: dependencies: esbuild: 0.23.0 - get-tsconfig: 4.7.6 + get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 @@ -31240,6 +31484,27 @@ snapshots: - supports-color - terser + vite-node@2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@10.0.0) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.1.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0): dependencies: cac: 6.7.14 @@ -31309,6 +31574,23 @@ snapshots: tsx: 3.12.2 yaml: 2.9.0 + vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + terser: 5.46.1 + tsx: 4.17.0 + yaml: 2.9.0 + vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0): dependencies: esbuild: 0.25.12 @@ -31343,6 +31625,44 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 + vitest@2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3(supports-color@10.0.0) + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) + vite-node: 2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.7)(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.7 From 026a2926861d5182252a4cb17eed25938ba866dd Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 11:06:54 +0100 Subject: [PATCH 2/9] fix(webapp,slack): guard webhook composer path writes and fix package checks Guard the webhook console path builder against prototype-pollution keys (__proto__, constructor, prototype) flagged by static analysis. Route webhook delivery run lookups through the run store instead of the control-plane replica, matching how run tables are read elsewhere. Update @trigger.dev/slack to a type-export checker compatible with the current TypeScript, keep compiled test files out of its build, and format two JSON fixtures. --- .../webhookConsole/WebhookComposer.tsx | 5 + .../WebhookDeliveriesListPresenter.server.ts | 10 +- .../WebhookDeliveryDetailPresenter.server.ts | 6 +- .../v3/WebhookDetailPresenter.server.ts | 9 +- .../webhook-sources/catalog/providers.json | 19 +- .../src/generated/hookdeck-samples.json | 309 ++++-------------- packages/slack/package.json | 5 +- packages/slack/tsconfig.src.json | 4 +- pnpm-lock.yaml | 37 +-- 9 files changed, 98 insertions(+), 306 deletions(-) diff --git a/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx b/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx index c8dc0fbf2e7..436eadc439a 100644 --- a/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx +++ b/apps/webapp/app/components/webhookConsole/WebhookComposer.tsx @@ -449,8 +449,13 @@ function HeadersEditor({ ); } +const UNSAFE_PATH_KEYS = new Set(["__proto__", "constructor", "prototype"]); + function setPath(target: Record, path: string, value: unknown) { const parts = path.split("."); + if (parts.some((part) => UNSAFE_PATH_KEYS.has(part))) { + return; + } let cursor = target; for (let i = 0; i < parts.length - 1; i++) { const key = parts[i]; diff --git a/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts index 046e9850429..b2c22e244b2 100644 --- a/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts @@ -2,6 +2,7 @@ import { type ClickHouse } from "@internal/clickhouse"; import { type PrismaClientOrTransaction, type WebhookDeliveryStatus } from "@trigger.dev/database"; import parseDuration from "parse-duration"; import { webhookReplica } from "~/db.server"; +import { runStore } from "~/v3/runStore.server"; import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; import { resolveDeliveryRunTargets, @@ -77,10 +78,11 @@ export class WebhookDeliveriesListPresenter { // Resolve it, and force empty results when no run matches. let internalRunId: string | undefined; if (runId) { - const run = await this.replica.taskRun.findFirst({ - where: { friendlyId: runId }, - select: { id: true }, - }); + const run = await runStore.findRun( + { friendlyId: runId }, + { select: { id: true } }, + this.replica + ); internalRunId = run?.id ?? "__none__"; } diff --git a/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts index 0f4d3ceed04..e64ed67ec10 100644 --- a/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/WebhookDeliveryDetailPresenter.server.ts @@ -6,6 +6,7 @@ import { type WebhookDeliveryStatus, } from "@trigger.dev/database"; import { webhookReplica } from "~/db.server"; +import { runStore } from "~/v3/runStore.server"; import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; export type WebhookDeliveryDetail = { @@ -80,10 +81,7 @@ export class WebhookDeliveryDetailPresenter { let session: { friendlyId: string; externalId: string | null } | null = null; if (delivery.runId) { const [taskRun, sessionRun] = await Promise.all([ - this.replica.taskRun.findFirst({ - where: { id: delivery.runId }, - select: { friendlyId: true }, - }), + runStore.findRun({ id: delivery.runId }, { select: { friendlyId: true } }, this.replica), this.replica.sessionRun.findUnique({ where: { runId: delivery.runId }, select: { session: { select: { friendlyId: true, externalId: true } } }, diff --git a/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts b/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts index c03547a6271..52eb4e2438a 100644 --- a/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/WebhookDetailPresenter.server.ts @@ -10,6 +10,7 @@ import parseDuration from "parse-duration"; import { z } from "zod"; import { type Direction } from "~/components/ListPagination"; import { webhookReplica } from "~/db.server"; +import { runStore } from "~/v3/runStore.server"; import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server"; import { webhookDeliveriesRepository } from "~/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server"; import { @@ -89,10 +90,10 @@ export async function resolveDeliveryRunTargets( if (runIds.length === 0) return { runFriendlyIdById, sessionByRunId }; const [runs, sessionRuns] = await Promise.all([ - replica.taskRun.findMany({ - where: { id: { in: runIds } }, - select: { id: true, friendlyId: true }, - }), + runStore.findRuns( + { where: { id: { in: runIds } }, select: { id: true, friendlyId: true } }, + replica + ), replica.sessionRun.findMany({ where: { runId: { in: runIds } }, select: { runId: true, session: { select: { friendlyId: true, externalId: true } } }, diff --git a/internal-packages/webhook-sources/catalog/providers.json b/internal-packages/webhook-sources/catalog/providers.json index 88bbda1640f..5aa11d0c07f 100644 --- a/internal-packages/webhook-sources/catalog/providers.json +++ b/internal-packages/webhook-sources/catalog/providers.json @@ -3,23 +3,10 @@ "scope": "ai-agent-first", "updatedAt": null, "definitionOfDone": { - "first-class": [ - "registryEntry", - "samples", - "roundTrip", - "producer" - ], - "sample-only": [ - "registryEntry", - "samples" - ] + "first-class": ["registryEntry", "samples", "roundTrip", "producer"], + "sample-only": ["registryEntry", "samples"] }, - "statusValues": [ - "not-started", - "in-progress", - "complete", - "blocked" - ], + "statusValues": ["not-started", "in-progress", "complete", "blocked"], "providers": [ { "id": "anthropic", diff --git a/internal-packages/webhook-sources/src/generated/hookdeck-samples.json b/internal-packages/webhook-sources/src/generated/hookdeck-samples.json index b6d7aa6016a..529a000bf2f 100644 --- a/internal-packages/webhook-sources/src/generated/hookdeck-samples.json +++ b/internal-packages/webhook-sources/src/generated/hookdeck-samples.json @@ -64,11 +64,7 @@ "eventDate": "2023-03-08T10:45:05+01:00", "merchantAccountCode": "OctovieECOM", "merchantReference": "8313842560770001", - "operations": [ - "CANCEL", - "CAPTURE", - "REFUND" - ], + "operations": ["CANCEL", "CAPTURE", "REFUND"], "paymentMethod": "visa", "pspReference": "NGAO2BVQFXJ7XZDP", "reason": "1234:7777:12/2012", @@ -1213,9 +1209,7 @@ "scope": "store/customer/channel/login/access/updated", "data": { "customer_id": 22, - "channel_ids": [ - 1 - ] + "channel_ids": [1] }, "hash": "3f9ea420af83450d7ef9f78b08c8af25b2213637" }, @@ -1368,10 +1362,7 @@ "data": { "type": "shared_modifier", "id": 205, - "affected_product_ids": [ - 1, - 2 - ], + "affected_product_ids": [1, 2], "context": { "channel_id": 2, "locale": "fr" @@ -1398,10 +1389,7 @@ "data": { "type": "local_option", "id": 205, - "affected_product_ids": [ - 1, - 2 - ], + "affected_product_ids": [1, 2], "context": { "channel_id": 2, "locale": "fr" @@ -1645,9 +1633,7 @@ "scope": "store/priceLists/deleted", "store_id": "1001197568", "data": { - "price_list_ids": [ - 5 - ] + "price_list_ids": [5] }, "hash": "e0c298b8097a6a2f39d17e593a9b360f5b2fef7d", "created_at": 1709327895, @@ -1697,11 +1683,7 @@ "data": { "type": "product", "id": 205, - "properties": [ - "warranty", - "is_featured", - "custom_fields" - ], + "properties": ["warranty", "is_featured", "custom_fields"], "context": { "channel_id": 2, "locale": "fr" @@ -4211,11 +4193,7 @@ } }, "type": "branch", - "merge_strategies": [ - "merge_commit", - "squash", - "fast_forward" - ], + "merge_strategies": ["merge_commit", "squash", "fast_forward"], "default_merge_strategy": "merge_commit" }, "new": { @@ -4302,11 +4280,7 @@ } }, "type": "branch", - "merge_strategies": [ - "merge_commit", - "squash", - "fast_forward" - ], + "merge_strategies": ["merge_commit", "squash", "fast_forward"], "default_merge_strategy": "merge_commit" }, "truncated": false, @@ -6684,9 +6658,7 @@ "id": 403738602, "name": "web", "active": true, - "events": [ - "*" - ], + "events": ["*"], "config": { "content_type": "json", "insecure_ssl": "0", @@ -7579,9 +7551,7 @@ }, "added": [], "removed": [], - "modified": [ - "README.md" - ] + "modified": ["README.md"] } }, "extraHeaders": { @@ -8535,9 +8505,7 @@ "email": "hookdeckwrite@gmail.com" }, "added": [], - "modified": [ - "README.md" - ], + "modified": ["README.md"], "removed": [] } ], @@ -11415,10 +11383,7 @@ "id": "550e8400-e29b-41d4-a716-446655440001", "title": "Draft Post", "status": "archived", - "channels": [ - "LinkedIn", - "Twitter" - ], + "channels": ["LinkedIn", "Twitter"], "campaign": null, "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", "labels": [ @@ -11544,10 +11509,7 @@ "post": { "id": "550e8400-e29b-41d4-a716-446655440001", "title": "Q4 Product Launch Announcement", - "channels": [ - "LinkedIn", - "Twitter" - ], + "channels": ["LinkedIn", "Twitter"], "campaign": { "id": "550e8400-e29b-41d4-a716-446655440003", "name": "Launch 2025", @@ -11630,9 +11592,7 @@ "id": "550e8400-e29b-41d4-a716-446655440001", "title": "New Draft", "status": "draft", - "channels": [ - "LinkedIn" - ], + "channels": ["LinkedIn"], "campaign": null, "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", "labels": [ @@ -11744,9 +11704,7 @@ "id": "550e8400-e29b-41d4-a716-446655440001", "title": "Old Draft", "status": "archived", - "channels": [ - "LinkedIn" - ], + "channels": ["LinkedIn"], "campaign": null, "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", "labels": [ @@ -11901,9 +11859,7 @@ "post": { "id": "550e8400-e29b-41d4-a716-446655440001", "title": "Q4 Product Launch Announcement", - "channels": [ - "LinkedIn" - ], + "channels": ["LinkedIn"], "campaign": null, "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", "labels": [ @@ -11970,10 +11926,7 @@ "post": { "id": "550e8400-e29b-41d4-a716-446655440001", "title": "Q4 Product Launch Announcement", - "channels": [ - "LinkedIn", - "Twitter" - ], + "channels": ["LinkedIn", "Twitter"], "campaign": { "id": "550e8400-e29b-41d4-a716-446655440003", "name": "Launch 2025", @@ -12068,9 +12021,7 @@ "post": { "id": "550e8400-e29b-41d4-a716-446655440001", "title": "Q4 Product Launch Announcement", - "channels": [ - "LinkedIn" - ], + "channels": ["LinkedIn"], "campaign": null, "url": "https://app.tryordinal.com/acme/posts/550e8400-e29b-41d4-a716-446655440001", "labels": [ @@ -17786,10 +17737,7 @@ }, "seller_protection": { "status": "ELIGIBLE", - "dispute_categories": [ - "ITEM_NOT_RECEIVED", - "UNAUTHORIZED_TRANSACTION" - ] + "dispute_categories": ["ITEM_NOT_RECEIVED", "UNAUTHORIZED_TRANSACTION"] }, "final_capture": true, "seller_receivable_breakdown": { @@ -18394,9 +18342,7 @@ ] }, "primary_currency_code": "USD", - "secondary_currency_codes": [ - "CAD" - ], + "secondary_currency_codes": ["CAD"], "financial_instruments": { "banks": [ { @@ -18973,9 +18919,7 @@ "current": [] }, "object": "activity", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841422, "timestamp_micro": 1680841422482110, @@ -19089,9 +19033,7 @@ "current": [] }, "object": "deal", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680840600, "timestamp_micro": 1680840600460452, @@ -19192,9 +19134,7 @@ "current": [] }, "object": "organization", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680840510, "timestamp_micro": 1680840510932610, @@ -19286,9 +19226,7 @@ "current": [] }, "object": "person", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680840511, "timestamp_micro": 1680840511329055, @@ -19388,9 +19326,7 @@ "current": [] }, "object": "product", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841164, "timestamp_micro": 1680841164749820, @@ -19461,9 +19397,7 @@ "current": [] }, "object": "activity", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841432, "timestamp_micro": 1680841432612516, @@ -19574,9 +19508,7 @@ "current": [] }, "object": "person", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841339, "timestamp_micro": 1680841339437193, @@ -19675,9 +19607,7 @@ "current": [] }, "object": "activity", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841419, "timestamp_micro": 1680841419576234, @@ -19881,9 +19811,7 @@ "current": [] }, "object": "deal", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680840600, "timestamp_micro": 1680840600882126, @@ -20047,9 +19975,7 @@ "current": [] }, "object": "organization", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841432, "timestamp_micro": 1680841432637031, @@ -20191,9 +20117,7 @@ "current": [] }, "object": "person", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841432, "timestamp_micro": 1680841432652693, @@ -20351,9 +20275,7 @@ "current": [] }, "object": "product", - "permitted_user_ids": [ - 14542087 - ], + "permitted_user_ids": [14542087], "pipedrive_service_name": false, "timestamp": 1680841165, "timestamp_micro": 1680841165016475, @@ -20825,9 +20747,7 @@ "multi_location_enabled": true, "setup_required": false, "pre_launch_enabled": false, - "enabled_presentment_currencies": [ - "USD" - ], + "enabled_presentment_currencies": ["USD"], "marketing_sms_consent_enabled_at_checkout": false, "transactional_sms_disabled": false }, @@ -22126,10 +22046,7 @@ "note": "Head Office Location", "buyer_experience_configuration": null, "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", - "tax_exemptions": [ - "CA_BC_CONTRACTOR_EXEMPTION", - "CA_BC_RESELLER_EXEMPTION" - ], + "tax_exemptions": ["CA_BC_CONTRACTOR_EXEMPTION", "CA_BC_RESELLER_EXEMPTION"], "company": { "name": "Example Company", "note": "This is an example company", @@ -22231,10 +22148,7 @@ "note": "Head Office Location", "buyer_experience_configuration": null, "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", - "tax_exemptions": [ - "CA_BC_CONTRACTOR_EXEMPTION", - "CA_BC_RESELLER_EXEMPTION" - ], + "tax_exemptions": ["CA_BC_CONTRACTOR_EXEMPTION", "CA_BC_RESELLER_EXEMPTION"], "company": { "name": "Example Company", "note": "This is an example company", @@ -22425,10 +22339,7 @@ "note": "Head Office Location", "buyer_experience_configuration": null, "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", - "tax_exemptions": [ - "CA_BC_CONTRACTOR_EXEMPTION", - "CA_BC_RESELLER_EXEMPTION" - ], + "tax_exemptions": ["CA_BC_CONTRACTOR_EXEMPTION", "CA_BC_RESELLER_EXEMPTION"], "company": { "name": "Example Company", "note": "This is an example company", @@ -22507,10 +22418,7 @@ "note": "Head Office Location", "buyer_experience_configuration": null, "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", - "tax_exemptions": [ - "CA_BC_CONTRACTOR_EXEMPTION", - "CA_BC_RESELLER_EXEMPTION" - ], + "tax_exemptions": ["CA_BC_CONTRACTOR_EXEMPTION", "CA_BC_RESELLER_EXEMPTION"], "company": { "name": "Example Company", "note": "This is an example company", @@ -22589,10 +22497,7 @@ "note": "Head Office Location", "buyer_experience_configuration": null, "admin_graphql_api_id": "gid://shopify/CompanyLocation/408372092144951419", - "tax_exemptions": [ - "CA_BC_CONTRACTOR_EXEMPTION", - "CA_BC_RESELLER_EXEMPTION" - ], + "tax_exemptions": ["CA_BC_CONTRACTOR_EXEMPTION", "CA_BC_RESELLER_EXEMPTION"], "company": { "name": "Example Company", "note": "This is an example company", @@ -23034,9 +22939,7 @@ "status": "failed", "errors": [ { - "customer_ids": [ - 1 - ], + "customer_ids": [1], "field": "merge_in_progress", "message": "John Doe is currently being merged." } @@ -24651,9 +24554,7 @@ } ], "tracking_number": "1z827wk74630", - "tracking_numbers": [ - "1z827wk74630" - ], + "tracking_numbers": ["1z827wk74630"], "tracking_url": "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630", "tracking_urls": [ "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630" @@ -24835,9 +24736,7 @@ } ], "tracking_number": "1z827wk74630", - "tracking_numbers": [ - "1z827wk74630" - ], + "tracking_numbers": ["1z827wk74630"], "tracking_url": "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630", "tracking_urls": [ "https://www.ups.com/WebTracking?loc=en_US&requester=ST&trackNums=1z827wk74630" @@ -25562,10 +25461,7 @@ "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", "original_total_additional_fees_set": null, "original_total_duties_set": null, - "payment_gateway_names": [ - "visa", - "bogus" - ], + "payment_gateway_names": ["visa", "bogus"], "phone": null, "po_number": null, "presentment_currency": "USD", @@ -25975,10 +25871,7 @@ "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", "original_total_additional_fees_set": null, "original_total_duties_set": null, - "payment_gateway_names": [ - "visa", - "bogus" - ], + "payment_gateway_names": ["visa", "bogus"], "phone": null, "po_number": null, "presentment_currency": "USD", @@ -26463,10 +26356,7 @@ "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", "original_total_additional_fees_set": null, "original_total_duties_set": null, - "payment_gateway_names": [ - "visa", - "bogus" - ], + "payment_gateway_names": ["visa", "bogus"], "phone": null, "po_number": null, "presentment_currency": "USD", @@ -26876,10 +26766,7 @@ "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", "original_total_additional_fees_set": null, "original_total_duties_set": null, - "payment_gateway_names": [ - "visa", - "bogus" - ], + "payment_gateway_names": ["visa", "bogus"], "phone": null, "po_number": null, "presentment_currency": "USD", @@ -27289,10 +27176,7 @@ "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", "original_total_additional_fees_set": null, "original_total_duties_set": null, - "payment_gateway_names": [ - "visa", - "bogus" - ], + "payment_gateway_names": ["visa", "bogus"], "phone": null, "po_number": null, "presentment_currency": "USD", @@ -27751,10 +27635,7 @@ "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", "original_total_additional_fees_set": null, "original_total_duties_set": null, - "payment_gateway_names": [ - "visa", - "bogus" - ], + "payment_gateway_names": ["visa", "bogus"], "phone": null, "po_number": null, "presentment_currency": "USD", @@ -28280,21 +28161,14 @@ "options": [ { "name": "Title", - "values": [ - "151cm", - "155cm", - "158cm" - ] + "values": ["151cm", "155cm", "158cm"] } ], "seo": { "title": "seo title", "description": "seo description" }, - "tags": [ - "tag1", - "tag2" - ], + "tags": ["tag1", "tag2"], "variants": { "edges": [ { @@ -28434,21 +28308,14 @@ "options": [ { "name": "Title", - "values": [ - "151cm", - "155cm", - "158cm" - ] + "values": ["151cm", "155cm", "158cm"] } ], "seo": { "title": "seo title", "description": "seo description" }, - "tags": [ - "tag1", - "tag2" - ], + "tags": ["tag1", "tag2"], "variants": { "edges": [ { @@ -28612,10 +28479,7 @@ "name": "Title", "product_id": 788032119674292900, "position": 1, - "values": [ - "Small", - "Medium" - ] + "values": ["Small", "Medium"] } ] } @@ -28743,10 +28607,7 @@ "name": "Title", "product_id": 788032119674292900, "position": 1, - "values": [ - "Small", - "Medium" - ] + "values": ["Small", "Medium"] } ] } @@ -29820,10 +29681,7 @@ "name": "Title", "product_id": 788032119674292900, "position": 1, - "values": [ - "Small", - "Medium" - ] + "values": ["Small", "Medium"] } ] } @@ -29951,10 +29809,7 @@ "name": "Title", "product_id": 788032119674292900, "position": 1, - "values": [ - "Small", - "Medium" - ] + "values": ["Small", "Medium"] } ] } @@ -30056,17 +29911,13 @@ "admin_graphql_api_app": "gid://shopify/App/2525000003", "app_id": null, "description": null, - "options": [ - "Delivery every" - ], + "options": ["Delivery every"], "position": null, "summary": "1 delivery frequency, discount", "selling_plans": [ { "name": "Pay every month deliver every month", - "options": [ - "month" - ], + "options": ["month"], "position": null, "description": null, "billing_policy": { @@ -30136,17 +29987,13 @@ "admin_graphql_api_app": "gid://shopify/App/2525000003", "app_id": null, "description": null, - "options": [ - "Delivery every" - ], + "options": ["Delivery every"], "position": null, "summary": "1 delivery frequency, discount", "selling_plans": [ { "name": "Pay every month deliver every month", - "options": [ - "month" - ], + "options": ["month"], "position": null, "description": null, "billing_policy": { @@ -30278,9 +30125,7 @@ "multi_location_enabled": true, "setup_required": false, "pre_launch_enabled": false, - "enabled_presentment_currencies": [ - "USD" - ], + "enabled_presentment_currencies": ["USD"], "marketing_sms_consent_enabled_at_checkout": false, "transactional_sms_disabled": false }, @@ -31131,9 +30976,7 @@ "payment_link": "plink_1MhUOwE0b6fckueSjmvf9Yea", "payment_method_collection": "always", "payment_method_options": {}, - "payment_method_types": [ - "card" - ], + "payment_method_types": ["card"], "payment_status": "paid", "phone_number_collection": { "enabled": false @@ -31213,9 +31056,7 @@ "name": "Martin", "next_invoice_sequence": 2, "phone": null, - "preferred_locales": [ - "en-US" - ], + "preferred_locales": ["en-US"], "shipping": null, "tax_exempt": "none", "test_clock": null @@ -31451,9 +31292,7 @@ "name": "Martin", "next_invoice_sequence": 2, "phone": null, - "preferred_locales": [ - "en-US" - ], + "preferred_locales": ["en-US"], "shipping": null, "tax_exempt": "none", "test_clock": null @@ -32530,9 +32369,7 @@ "generated_from": null, "last4": "4242", "networks": { - "available": [ - "visa" - ], + "available": ["visa"], "preferred": null }, "three_d_secure_usage": { @@ -32598,9 +32435,7 @@ "request_three_d_secure": "automatic" } }, - "payment_method_types": [ - "card" - ], + "payment_method_types": ["card"], "single_use_mandate": null, "status": "succeeded", "usage": "off_session" @@ -32657,9 +32492,7 @@ "request_three_d_secure": "automatic" } }, - "payment_method_types": [ - "card" - ], + "payment_method_types": ["card"], "single_use_mandate": null, "status": "succeeded", "usage": "off_session" @@ -32902,10 +32735,7 @@ "grouped_products": [], "menu_order": 0, "price_html": "200 CFA 150 CFA", - "related_ids": [ - 13, - 12 - ], + "related_ids": [13, 12], "meta_data": [], "stock_status": "instock", "has_options": false, @@ -33013,10 +32843,7 @@ "grouped_products": [], "menu_order": 0, "price_html": "200 CFA 150 CFA", - "related_ids": [ - 12, - 13 - ], + "related_ids": [12, 13], "meta_data": [], "stock_status": "instock", "has_options": false, diff --git a/packages/slack/package.json b/packages/slack/package.json index ee258950a11..db739e2e216 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -20,6 +20,9 @@ "main": true, "module": true, "project": "./tsconfig.src.json", + "exclude": [ + "./src/**/*.test.ts" + ], "exports": { "./package.json": "./package.json", ".": "./src/index.ts" @@ -44,7 +47,7 @@ "@trigger.dev/sdk": "workspace:^4.5.0-rc.7" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.15.4", + "@arethetypeswrong/cli": "^0.18.5", "@trigger.dev/sdk": "workspace:4.5.0-rc.7", "rimraf": "6.0.1", "tshy": "^3.0.2", diff --git a/packages/slack/tsconfig.src.json b/packages/slack/tsconfig.src.json index db06c53317e..19b058321cb 100644 --- a/packages/slack/tsconfig.src.json +++ b/packages/slack/tsconfig.src.json @@ -1,10 +1,12 @@ { "extends": "./tsconfig.json", "include": ["./src/**/*.ts"], + "exclude": ["./src/**/*.test.ts"], "compilerOptions": { "isolatedDeclarations": false, "composite": true, "sourceMap": true, - "customConditions": ["@triggerdotdev/source"] + "customConditions": ["@triggerdotdev/source"], + "types": ["node"] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da2dd01e221..9a4f643faaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2146,8 +2146,8 @@ importers: version: link:../core devDependencies: '@arethetypeswrong/cli': - specifier: ^0.15.4 - version: 0.15.4 + specifier: ^0.18.5 + version: 0.18.5 '@trigger.dev/sdk': specifier: workspace:4.5.0-rc.7 version: link:../trigger-sdk @@ -2355,20 +2355,11 @@ packages: '@antfu/utils@9.3.0': resolution: {integrity: sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==} - '@arethetypeswrong/cli@0.15.4': - resolution: {integrity: sha512-YDbImAi1MGkouT7f2yAECpUMFhhA1J0EaXzIqoC5GGtK0xDgauLtcsZezm8tNq7d3wOFXH7OnY+IORYcG212rw==} - engines: {node: '>=18'} - hasBin: true - '@arethetypeswrong/cli@0.18.5': resolution: {integrity: sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==} engines: {node: '>=20'} hasBin: true - '@arethetypeswrong/core@0.15.1': - resolution: {integrity: sha512-FYp6GBAgsNz81BkfItRz8RLZO03w5+BaeiPma1uCfmxTnxbtuMrI/dbzGiOk8VghO108uFI0oJo0OkewdSHw7g==} - engines: {node: '>=18'} - '@arethetypeswrong/core@0.18.5': resolution: {integrity: sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==} engines: {node: '>=20'} @@ -14834,9 +14825,6 @@ packages: typescript: optional: true - ts-expose-internals-conditionally@1.0.0-empty.0: - resolution: {integrity: sha512-F8m9NOF6ZhdOClDVdlM8gj3fDCav4ZIFSs/EI3ksQbAAXVSCN/Jh5OCJDDZWBuBy9psFc6jULGDlPwjMYMhJDw==} - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -15892,16 +15880,6 @@ snapshots: '@antfu/utils@9.3.0': {} - '@arethetypeswrong/cli@0.15.4': - dependencies: - '@arethetypeswrong/core': 0.15.1 - chalk: 4.1.2 - cli-table3: 0.6.5 - commander: 10.0.1 - marked: 9.1.6 - marked-terminal: 7.1.0(marked@9.1.6) - semver: 7.8.1 - '@arethetypeswrong/cli@0.18.5': dependencies: '@arethetypeswrong/core': 0.18.5 @@ -15912,15 +15890,6 @@ snapshots: marked-terminal: 7.1.0(marked@9.1.6) semver: 7.8.1 - '@arethetypeswrong/core@0.15.1': - dependencies: - '@andrewbranch/untar.js': 1.0.3 - fflate: 0.8.3 - semver: 7.8.1 - ts-expose-internals-conditionally: 1.0.0-empty.0 - typescript: 6.0.3 - validate-npm-package-name: 5.0.0 - '@arethetypeswrong/core@0.18.5': dependencies: '@andrewbranch/untar.js': 1.0.3 @@ -30905,8 +30874,6 @@ snapshots: optionalDependencies: typescript: 6.0.3 - ts-expose-internals-conditionally@1.0.0-empty.0: {} - ts-interface-checker@0.1.13: {} ts-pattern@3.3.5: {} From 289bd409a700836e98f141b7cf2bec116f271ca1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 11:43:13 +0100 Subject: [PATCH 3/9] fix(webapp): gate remaining webhook dashboard routes behind the feature flag The webhook handler detail page and the deliveries-live and samples resource loaders were reachable by direct URL without the hasWebhooksAccess flag. Add the same org-flag guard the other webhook routes use, so the whole dashboard surface stays hidden until the flag is enabled (admins and impersonators still pass). --- .../route.tsx | 15 +++++++++++++++ ...am.env.$envParam.webhooks.deliveries.live.ts | 17 +++++++++++++++++ ...ojectParam.env.$envParam.webhooks.samples.ts | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx index 2d65d81941c..0496005637f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx @@ -52,6 +52,8 @@ import { } from "~/presenters/v3/WebhookDetailPresenter.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { requireUser } from "~/services/session.server"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; import { docsPath, EnvironmentParamSchema, @@ -88,6 +90,19 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { throw new Response("Environment not found", { status: 404 }); } + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) throw new Response("Not found", { status: 404 }); + } + const url = new URL(request.url); const period = url.searchParams.get("period") ?? undefined; const from = parseFiniteInt(url.searchParams.get("from")); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts index f6f529aa668..2e712d35217 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts @@ -5,6 +5,9 @@ import { $replica, webhookReplica } from "~/db.server"; import { resolveDeliveryRunTargets } from "~/presenters/v3/WebhookDetailPresenter.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { loadProjectEnvironmentFromRequest } from "~/services/loadProjectEnvironmentFromRequest.server"; +import { requireUser } from "~/services/session.server"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; import { type ListedWebhookDelivery, webhookDeliveriesRepository, @@ -75,6 +78,20 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { project, environment } = await loadProjectEnvironmentFromRequest(request, params); + const user = await requireUser(request); + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) throw new Response("Not found", { status: 404 }); + } + const clickhouse = await clickhouseFactory.getClickhouseForOrganization( project.organizationId, "standard" diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts index 0b253e1be4f..fdd25ee2aa9 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.samples.ts @@ -7,9 +7,12 @@ import { } from "@internal/webhook-sources"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson } from "remix-typedjson"; +import { $replica } from "~/db.server"; import { findProjectBySlug } from "~/models/project.server"; import { requireUser } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { FEATURE_FLAG } from "~/v3/featureFlags"; +import { flag } from "~/v3/featureFlags.server"; export type WebhookSampleMeta = { provider: string; @@ -49,6 +52,19 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!project) return typedjson({ kind: "error", error: "Project not found" } as WebhookSamplesData); + if (!user.admin && !user.isImpersonating) { + const org = await $replica.organization.findFirst({ + where: { id: project.organizationId }, + select: { featureFlags: true }, + }); + const enabled = await flag({ + key: FEATURE_FLAG.hasWebhooksAccess, + defaultValue: false, + overrides: (org?.featureFlags as Record) ?? {}, + }); + if (!enabled) throw new Response("Not found", { status: 404 }); + } + const url = new URL(request.url); const provider = url.searchParams.get("provider") ?? undefined; const eventType = url.searchParams.get("eventType") ?? undefined; From a15a2f93cfe98a4f79fadc841a898814cdae4bf2 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 13:45:13 +0100 Subject: [PATCH 4/9] fix(webapp,core): resolve lint and type-export checks on the webhook branch Clean up unused and duplicate imports across the webhook files that oxlint flagged (they were never linted before). Add the missing `webhooks` entry to core's typesVersions so the `@trigger.dev/core/webhooks` subpath passes the package type-export check. Move the webhook nav color into the Tailwind v4 CSS theme (--color-webhooks) and drop the stale tailwind.config.js the v4 migration removed. --- .../v1/WebhookDeliveryFilters.tsx | 6 - .../route.tsx | 2 - .../route.tsx | 2 - .../route.tsx | 12 +- .../app/services/realtime/sessions.server.ts | 2 +- .../webhookDeliveriesRepository.server.ts | 7 +- apps/webapp/app/tailwind.css | 1 + apps/webapp/tailwind.config.js | 387 ------------------ .../clickhouse/src/webhookDeliveries.ts | 4 +- .../webhook-engine/src/engine/index.ts | 23 +- .../src/engine/partitions.test.ts | 2 +- .../webhook-engine/src/engine/types.ts | 8 +- .../src/engine/verification/index.ts | 9 +- packages/core/package.json | 3 + packages/core/src/v3/types/webhooks.ts | 1 - packages/trigger-sdk/src/v3/ai.ts | 17 +- packages/trigger-sdk/src/v3/webhooks.ts | 12 +- 17 files changed, 46 insertions(+), 452 deletions(-) delete mode 100644 apps/webapp/tailwind.config.js diff --git a/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx b/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx index 33f91662dc5..aa6d9edd9fd 100644 --- a/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx +++ b/apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx @@ -17,12 +17,6 @@ import { shortcutFromIndex, } from "~/components/primitives/Select"; import { ShortcutKey } from "~/components/primitives/ShortcutKey"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/primitives/Tooltip"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { useSearchParams } from "~/hooks/useSearchParam"; import { useShortcutKeys } from "~/hooks/useShortcutKeys"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index 151d56534eb..55386db45dc 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -52,8 +52,6 @@ import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource"; import { TimezoneList } from "~/components/scheduled/timezones"; import { $replica } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { redirectBackWithErrorMessage, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx index 0496005637f..a8b7fca318a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.$webhookParam/route.tsx @@ -58,8 +58,6 @@ import { docsPath, EnvironmentParamSchema, v3EnvironmentPath, - v3RunPath, - v3SessionPath, v3WebhookDeliveryPath, } from "~/utils/pathBuilder"; import { parseFiniteInt } from "~/utils/searchParams"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx index ae011d6a091..8e77945cc95 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.endpoints.$endpointParam/route.tsx @@ -4,8 +4,7 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/ser import { randomBytes } from "node:crypto"; import { WebhookRoutingTarget, WebhookVerifierArtifact } from "@trigger.dev/core/v3"; import type { WebhookValueSource } from "@trigger.dev/core/v3"; -import { useEffect, useState } from "react"; -import { Suspense } from "react"; +import { Suspense, useEffect, useState } from "react"; import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { WebhookIcon } from "~/assets/icons/WebhookIcon"; @@ -48,12 +47,7 @@ import { import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { getSecretStore } from "~/services/secrets/secretStore.server"; import { requireUser } from "~/services/session.server"; -import { - docsPath, - EnvironmentParamSchema, - v3WebhookTaskPath, - v3WebhooksPath, -} from "~/utils/pathBuilder"; +import { docsPath, EnvironmentParamSchema, v3WebhookTaskPath } from "~/utils/pathBuilder"; import { parseFiniteInt } from "~/utils/searchParams"; import { FEATURE_FLAG } from "~/v3/featureFlags"; import { flag } from "~/v3/featureFlags.server"; @@ -98,7 +92,7 @@ async function requireWebhookAccess(request: Request, params: LoaderFunctionArgs } export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { user, project, environment, endpointParam } = await requireWebhookAccess(request, params); + const { project, environment, endpointParam } = await requireWebhookAccess(request, params); const clickhouse = await clickhouseFactory.getClickhouseForOrganization( project.organizationId, diff --git a/apps/webapp/app/services/realtime/sessions.server.ts b/apps/webapp/app/services/realtime/sessions.server.ts index 2dd60692b2b..45eda11b755 100644 --- a/apps/webapp/app/services/realtime/sessions.server.ts +++ b/apps/webapp/app/services/realtime/sessions.server.ts @@ -5,7 +5,7 @@ import type { RunStore } from "@internal/run-store"; import { $replica, prisma } from "~/db.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { chatSnapshotStoragePathForSession } from "~/services/realtime/chatSnapshot.server"; -import { runStore, runStore as defaultRunStore } from "~/v3/runStore.server"; +import { runStore as defaultRunStore } from "~/v3/runStore.server"; /** * Prefix that {@link SessionId.generate} attaches to every Session friendlyId. diff --git a/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts b/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts index 8337ee3c080..32a9b4923bc 100644 --- a/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts +++ b/apps/webapp/app/services/webhookDeliveriesRepository/webhookDeliveriesRepository.server.ts @@ -1,8 +1,7 @@ import { type ClickHouse } from "@internal/clickhouse"; import { type Tracer } from "@internal/tracing"; import { type Logger, type LogLevel } from "@trigger.dev/core/logger"; -import { type Prisma, WebhookDeliveryStatus } from "@trigger.dev/database"; -import { z } from "zod"; +import { type Prisma, type WebhookDeliveryStatus } from "@trigger.dev/database"; import { type WebhookReplicaDatabase } from "~/db.server"; import { startActiveSpan } from "~/v3/tracer.server"; import { ClickHouseWebhookDeliveriesRepository } from "./clickhouseWebhookDeliveriesRepository.server"; @@ -15,10 +14,6 @@ export type WebhookDeliveriesRepositoryOptions = { tracer?: Tracer; }; -const DeliveryStatus = z.enum( - Object.values(WebhookDeliveryStatus) as [WebhookDeliveryStatus, ...WebhookDeliveryStatus[]] -); - export type FilterWebhookDeliveriesOptions = { organizationId: string; projectId: string; diff --git a/apps/webapp/app/tailwind.css b/apps/webapp/app/tailwind.css index 2bc91d77323..633a86b503b 100644 --- a/apps/webapp/app/tailwind.css +++ b/apps/webapp/app/tailwind.css @@ -202,6 +202,7 @@ --color-aiMetrics: var(--color-green-500); --color-errors: var(--color-amber-500); --color-agents: var(--color-purple-500); + --color-webhooks: var(--color-teal-500); --color-sessions: var(--color-pink-500); --color-playgrounds: var(--color-fuchsia-500); --color-models: var(--color-violet-500); diff --git a/apps/webapp/tailwind.config.js b/apps/webapp/tailwind.config.js deleted file mode 100644 index 43f16718a30..00000000000 --- a/apps/webapp/tailwind.config.js +++ /dev/null @@ -1,387 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -const colors = require("tailwindcss/colors"); - -// V2 -const slate = { - 450: "#7E8FA6", - 500: "#6B7C95", - 550: "#586981", - 600: "#45566D", - 650: "#3C4B62", - 750: "#293649", - 850: "#1A2434", - 900: "#131B2B", - 950: "#0E1521", - 1000: "#0B1018", -}; - -const toxic = { - 50: "#E3FFE6", - 100: "#C8FFCD", - 200: "#A9FFAB", - 300: "#8AFF96", - 400: "#6DFC7B", - 500: "#41FF54", - 600: "#28F03C", - 700: "#2AE03C", - 800: "#22D834", - 900: "#16CC28", -}; - -const acid = { - 50: "#F9FFD1", - 100: "#F6FFB6", - 200: "#F3FF99", - 300: "#EEFF82", - 400: "#E7FF52", - 500: "#DAF437", - 600: "#C5E118", - 700: "#B2CD0A", - 800: "#A5BE07", - 900: "#9FB802", -}; - -const midnight = { - ...colors.slate, - 450: colors.slate[850], - 500: colors.slate[650], - 550: colors.slate[700], - 600: colors.slate[750], - 650: colors.slate[800], - 750: colors.slate[850], - 800: colors.slate[900], - 850: "#0E1521", - 900: "#0B1018", -}; - -// V3 -const charcoal = { - 100: "#E8E9EC", - 200: "#D7D9DD", - 300: "#B5B8C0", - 400: "#878C99", - 500: "#5F6570", - 550: "#4D525B", - 600: "#3B3E45", - 650: "#2C3034", - 700: "#272A2E", - 750: "#212327", - 775: "#1C1E21", - 800: "#1A1B1F", - 850: "#15171A", - 900: "#121317", - 950: "#0D0E12", - 1000: "#0B0C0F", -}; - -const apple = { - 100: "#E4FFC9", - 200: "#CFFFA0", - 300: "#BFFF81", - 400: "#AFFF62", - 500: "#A8FF53", - 600: "#82D134", - 700: "#6FB12F", - 750: "#5E932A", - 800: "#45711A", - 850: "#2E4E10", - 900: "#20370A", - 950: "#152506", -}; - -const mint = { - 50: "#F0FDF4", - 100: "#DDFBE6", - 200: "#BDF5D0", - 300: "#87EBA9", - 400: "#4FD97E", - 500: "#28BF5C", - 600: "#1B9E48", - 700: "#197C3C", - 800: "#196233", - 900: "#16512C", - 950: "#062D15", -}; - -const sun = { - 50: "#FDFEE8", - 100: "#FDFFC2", - 200: "#FFFF89", - 300: "#FFF852", - 400: "#FDEA12", - 500: "#ECCF06", - 600: "#CCA302", - 700: "#A37505", - 800: "#865B0D", - 900: "#724B11", - 950: "#432705", -}; - -const lavender = { - 50: "##f4f2ff", - 100: "#eae8ff", - 200: "#d7d4ff", - 300: "#bab2ff", - 400: "#826dff", - 500: "#7655fd", - 600: "#6532f5", - 700: "#5620e1", - 800: "#481abd", - 900: "#3d189a", - 950: "#230c69", -}; - -/** Trigger.dev custom palette */ - -/** Text colors */ -const primary = apple[500]; -const secondary = charcoal[650]; -const tertiary = charcoal[700]; -const textLink = lavender[400]; -const textDimmed = charcoal[400]; -const textBright = charcoal[200]; -const backgroundBright = charcoal[800]; -const backgroundDimmed = charcoal[850]; -const gridBright = charcoal[700]; -const gridDimmed = charcoal[750]; -const success = mint[500]; -const pending = colors.blue[500]; -const warning = colors.amber[500]; -const error = colors.rose[600]; -const devEnv = colors.pink[500]; -const stagingEnv = colors.orange[400]; -const previewEnv = colors.yellow[400]; -const prodEnv = mint[500]; - -/** Icon colors */ -const tasks = colors.blue[500]; -const runs = colors.indigo[500]; -const batches = colors.pink[500]; -const schedules = colors.yellow[500]; -const queues = colors.purple[500]; -const query = colors.blue[500]; -const metrics = colors.green[500]; -const customDashboards = charcoal[400]; -const deployments = colors.blue[500]; -const concurrency = colors.amber[500]; -const limits = colors.purple[500]; -const regions = colors.green[500]; -const logs = colors.pink[500]; -const tests = colors.pink[500]; -const apiKeys = colors.amber[500]; -const environmentVariables = colors.pink[500]; -const alerts = colors.red[500]; -const projectSettings = colors.blue[500]; -const orgSettings = colors.blue[500]; -const docs = colors.blue[500]; -const bulkActions = colors.emerald[500]; -const aiPrompts = colors.blue[500]; -const aiMetrics = colors.green[500]; -const errors = colors.amber[500]; -const agents = colors.purple[500]; -const webhooks = colors.teal[500]; // distinct from tasks(blue)/schedules(yellow)/agents(purple) -const sessions = colors.pink[500]; -const playgrounds = colors.fuchsia[500]; -const models = colors.violet[500]; -const previewBranches = colors.yellow[500]; - -/** Other variables */ -const radius = "0.5rem"; - -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ["./app/**/*.{ts,jsx,tsx}", "./node_modules/streamdown/dist/**/*.js"], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - fontFamily: { - sans: ["Geist Variable", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"], - mono: ["Geist Mono Variable", "monaco", "Consolas", "Lucida Console", "monospace"], - }, - fontSize: { - xxs: [ - "0.65rem", - { - lineHeight: "0.75rem", - letterSpacing: "-0.01em", - fontWeight: "500", - }, - ], - "2sm": [ - "0.8125rem", - { - lineHeight: "0.875rem", - letterSpacing: "-0.01em", - fontWeight: "500", - }, - ], - }, - colors: { - charcoal, - apple, - lavender, - mint, - sun, - primary, - secondary, - tertiary, - "text-link": textLink, - "text-dimmed": textDimmed, - "text-bright": textBright, - "background-bright": backgroundBright, - "background-dimmed": backgroundDimmed, - "grid-bright": gridBright, - "grid-dimmed": gridDimmed, - success, - pending, - warning, - error, - dev: devEnv, - staging: stagingEnv, - prod: prodEnv, - preview: previewEnv, - tasks, - runs, - batches, - schedules, - concurrency, - queues, - query, - regions, - limits, - deployments, - logs, - tests, - apiKeys, - environmentVariables, - alerts, - projectSettings, - orgSettings, - docs, - bulkActions, - metrics, - customDashboards, - aiPrompts, - aiMetrics, - errors, - agents, - webhooks, - sessions, - playgrounds, - models, - previewBranches, - // shadcn/ui color tokens used by streamdown's internal components - // (link safety modal, code block actions, etc.) - // Values are defined via CSS variables in .streamdown-container - background: "hsl(var(--background, 230 16% 9%) / )", - foreground: "hsl(var(--foreground, 215 19% 87%) / )", - muted: { - DEFAULT: "hsl(var(--muted, 220 8% 17%) / )", - foreground: "hsl(var(--muted-foreground, 220 8% 57%) / )", - }, - border: "hsl(var(--border, 216 7% 27%) / )", - sidebar: "hsl(var(--sidebar, 228 10% 11%) / )", - "primary-foreground": "hsl(var(--primary-foreground, 230 16% 9%) / )", - }, - focusStyles: { - outline: "1px solid", - outlineOffset: "0px", - outlineColor: textLink, - borderRadius: "3px", - }, - borderRadius: { - lg: radius, - md: `calc(${radius} - 2px)`, - sm: `calc(${radius} - 4px)`, - }, - boxShadow: { - "glow-primary": "0 0 10px 5px rgba(218, 244, 55, 0.2)", - "glow-secondary": "0 0 10px 5px rgba(79, 70, 229, 0.2)", - "glow-pink": "0 0 10px 5px rgba(236, 72, 153, 0.2)", - }, - outlineWidth: { - 3: "3px", - }, - textShadow: { - custom: "1px 1px 1px rgba(0, 0, 0, 0.5)", // Offset-X | Offset-Y | Blur radius | Color - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - float: { - "0%": { transform: "translatey(0px)" }, - "50%": { transform: "translatey(7px)" }, - "100%": { transform: "translatey(0px)" }, - }, - "tile-move": { - "0%": { "background-position": "0px" }, - "100%": { "background-position": "8px" }, - }, - "tile-move-offset": { - "0%": { "background-position": "-1px" }, - "100%": { "background-position": "7px" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - "tile-scroll": "tile-move 0.5s infinite linear", - "tile-scroll-offset": "tile-move-offset 0.5s infinite linear", - }, - backgroundImage: { - "gradient-radial": "radial-gradient(closest-side, var(--tw-gradient-stops))", - "gradient-primary": `linear-gradient(90deg, acid-500 0%, toxic-500 100%)`, - "gradient-primary-hover": `linear-gradient(80deg, acid-600 0%, toxic-600 100%)`, - "gradient-secondary": `linear-gradient(90deg, hsl(271 91 65) 0%, hsl(221 83 53) 100%)`, - "gradient-radial-secondary ": `radial-gradient(hsl(271 91 65), hsl(221 83 53))`, - }, - gridTemplateColumns: { - carousel: "repeat(6, 200px)", - }, - screens: { - "lg-height": { raw: "(max-height: 750px)" }, - "md-height": { raw: "(max-height: 600px)" }, - }, - width: { - 0.75: "0.1875rem", - 4.5: "1.125rem", - }, - height: { - 0.75: "0.1875rem", - 4.5: "1.125rem", - }, - size: { - 4.5: "1.125rem", - }, - }, - }, - plugins: [ - require("@tailwindcss/container-queries"), - require("@tailwindcss/forms"), - require("@tailwindcss/typography"), - require("tailwindcss-animate"), - require("tailwind-scrollbar"), - require("tailwind-scrollbar-hide"), - require("tailwindcss-textshadow"), - function ({ addUtilities, theme }) { - const focusStyles = theme("focusStyles", {}); - addUtilities({ - ".focus-custom": { - "&:focus-visible": focusStyles, - }, - }); - }, - ], -}; diff --git a/internal-packages/clickhouse/src/webhookDeliveries.ts b/internal-packages/clickhouse/src/webhookDeliveries.ts index cf6554969a0..399ea0e1a7b 100644 --- a/internal-packages/clickhouse/src/webhookDeliveries.ts +++ b/internal-packages/clickhouse/src/webhookDeliveries.ts @@ -1,6 +1,6 @@ -import { ClickHouseSettings } from "@clickhouse/client"; +import type { ClickHouseSettings } from "@clickhouse/client"; import { z } from "zod"; -import { ClickhouseReader, ClickhouseWriter } from "./client/types.js"; +import type { ClickhouseReader, ClickhouseWriter } from "./client/types.js"; export const WebhookDeliveryV1 = z.object({ environment_id: z.string(), diff --git a/internal-packages/webhook-engine/src/engine/index.ts b/internal-packages/webhook-engine/src/engine/index.ts index c05820a0ff5..84e668f4b5a 100644 --- a/internal-packages/webhook-engine/src/engine/index.ts +++ b/internal-packages/webhook-engine/src/engine/index.ts @@ -1,16 +1,15 @@ -import { - Counter, - getMeter, - getTracer, - Histogram, - Meter, - startSpan, - Tracer, -} from "@internal/tracing"; +import type { Counter, Histogram, Meter, Tracer } from "@internal/tracing"; +import { getMeter, getTracer, startSpan } from "@internal/tracing"; import { Logger } from "@trigger.dev/core/logger"; -import { Prisma, WebhookDatabase, WebhookDelivery, WebhookEndpoint } from "@trigger.dev/database"; +import type { + Prisma, + WebhookDatabase, + WebhookDelivery, + WebhookEndpoint, +} from "@trigger.dev/database"; import { Worker, type JobHandlerParams } from "@trigger.dev/redis-worker"; -import { createRedisClient, Redis } from "@internal/redis"; +import type { Redis } from "@internal/redis"; +import { createRedisClient } from "@internal/redis"; import { FilterAst, WebhookVerifierArtifact } from "@trigger.dev/core/v3"; import type { WebhookRoutingTarget } from "@trigger.dev/core/v3"; import { WebhookDeliveryId } from "@trigger.dev/core/v3/isomorphic"; @@ -21,7 +20,7 @@ import { evaluateFilter, parseFilter } from "./filter/index.js"; import { verify } from "./verification/index.js"; import { sha256Hex } from "./verification/util.js"; import { deriveIdempotencyKey, tryParseJson } from "./verification/derive.js"; -import { +import type { IngestInput, IngestResult, ReplayResult, diff --git a/internal-packages/webhook-engine/src/engine/partitions.test.ts b/internal-packages/webhook-engine/src/engine/partitions.test.ts index d3f61f9bc92..fbc1288d138 100644 --- a/internal-packages/webhook-engine/src/engine/partitions.test.ts +++ b/internal-packages/webhook-engine/src/engine/partitions.test.ts @@ -1,5 +1,5 @@ import { postgresTest } from "@internal/testcontainers"; -import { PrismaClient } from "@trigger.dev/database"; +import type { PrismaClient } from "@trigger.dev/database"; import { expect } from "vitest"; import { addDays, diff --git a/internal-packages/webhook-engine/src/engine/types.ts b/internal-packages/webhook-engine/src/engine/types.ts index ed79ccaf764..7ea2a9a9583 100644 --- a/internal-packages/webhook-engine/src/engine/types.ts +++ b/internal-packages/webhook-engine/src/engine/types.ts @@ -1,7 +1,7 @@ -import { Logger } from "@trigger.dev/core/logger"; -import { Meter, Tracer } from "@internal/tracing"; -import { WebhookDatabase } from "@trigger.dev/database"; -import { RedisOptions } from "@internal/redis"; +import type { Logger } from "@trigger.dev/core/logger"; +import type { Meter, Tracer } from "@internal/tracing"; +import type { WebhookDatabase } from "@trigger.dev/database"; +import type { RedisOptions } from "@internal/redis"; export type WebhookDeliverTaskErrorType = "QUEUE_LIMIT" | "SYSTEM_ERROR" | "NOT_FOUND"; diff --git a/internal-packages/webhook-engine/src/engine/verification/index.ts b/internal-packages/webhook-engine/src/engine/verification/index.ts index 913254bf24e..198a0b8369c 100644 --- a/internal-packages/webhook-engine/src/engine/verification/index.ts +++ b/internal-packages/webhook-engine/src/engine/verification/index.ts @@ -1,9 +1,14 @@ -import type { VerifierArtifact, VerifierConfig, VerifierResult, VerifyInput } from "./types.js"; +import type { + SchemeVerifier, + VerifierArtifact, + VerifierConfig, + VerifierResult, + VerifyInput, +} from "./types.js"; import { hmacVerifier } from "./hmac.js"; import { asymmetricVerifier } from "./asymmetric.js"; import { sharedSecretVerifier } from "./sharedSecret.js"; import { urlSecretVerifier } from "./urlSecret.js"; -import type { SchemeVerifier } from "./types.js"; const SCHEME_REGISTRY: Record = { hmac: hmacVerifier, diff --git a/packages/core/package.json b/packages/core/package.json index fc691856ed0..936f285190f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -74,6 +74,9 @@ "schemas": [ "dist/commonjs/schemas/index.d.ts" ], + "webhooks": [ + "dist/commonjs/v3/webhooks/index.d.ts" + ], "types": [ "dist/commonjs/types.d.ts" ], diff --git a/packages/core/src/v3/types/webhooks.ts b/packages/core/src/v3/types/webhooks.ts index 52071e8db99..2485c4419b3 100644 --- a/packages/core/src/v3/types/webhooks.ts +++ b/packages/core/src/v3/types/webhooks.ts @@ -1,6 +1,5 @@ import type { WebhookVerifierArtifact, - WebhookRoutingTarget, WebhookSecretProvisioning, } from "../schemas/webhookConfig.js"; diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index 23da6be9bb6..04be875bf81 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -35,6 +35,14 @@ import { type TaskWithSchema, TRIGGER_CONTROL_SUBTYPE, type StreamWriteResult, + type AnyChatEvent, + type ChatEventActions, + type ValidatedWebhookKey, + type ValidateWebhookFilter, + type WebhookVerifierArtifact, + type WebhookSecretProvisioning, + type AnyWebhookSource, + type InferWebhookEvent, } from "@trigger.dev/core/v3"; import type { FinishReason, @@ -49,15 +57,6 @@ import type { JSONSchema7, Schema, } from "ai"; -import type { AnyChatEvent, ChatEventActions } from "@trigger.dev/core/v3"; -import type { - ValidatedWebhookKey, - ValidateWebhookFilter, - WebhookVerifierArtifact, - WebhookSecretProvisioning, - AnyWebhookSource, - InferWebhookEvent, -} from "@trigger.dev/core/v3"; import { chatEvent, normalizeKeyString } from "./webhooks.js"; // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require` // ESM-only `ai@7` (see ../imports/ai-runtime.ts). diff --git a/packages/trigger-sdk/src/v3/webhooks.ts b/packages/trigger-sdk/src/v3/webhooks.ts index cdbce6457fa..132d9563141 100644 --- a/packages/trigger-sdk/src/v3/webhooks.ts +++ b/packages/trigger-sdk/src/v3/webhooks.ts @@ -1,7 +1,4 @@ -import { Webhook } from "@trigger.dev/core/v3"; -import { subtle } from "../imports/uncrypto.js"; -import { createTask } from "./shared.js"; -import { resourceCatalog } from "@trigger.dev/core/v3"; +import { Webhook, resourceCatalog } from "@trigger.dev/core/v3"; import type { WebhookSource, InferWebhookEvent, @@ -10,12 +7,13 @@ import type { ValidateWebhookFilter, ChatEvent, ValidatedWebhookKey, -} from "@trigger.dev/core/v3"; -import type { WebhookVerifierConfig, StripeWebhookEvent, GitHubWebhookEvent, + TaskRunContext, } from "@trigger.dev/core/v3"; +import { subtle } from "../imports/uncrypto.js"; +import { createTask, type Task } from "./shared.js"; import { discordVerifierConfig, githubVerifierConfig, @@ -25,8 +23,6 @@ import { webhookProviderConfigs, type WebhookProviderId, } from "@trigger.dev/core/webhooks"; -import type { Task } from "./shared.js"; -import type { TaskRunContext } from "@trigger.dev/core/v3"; /** * The type of error thrown when a webhook fails to parse or verify From e91b4d74b7122d71e8d6d7c67f97ce80724d1483 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 13:59:37 +0100 Subject: [PATCH 5/9] fix(clickhouse): drop a comment that broke webhook_deliveries_v1 migration parsing The 034 migration had an inline comment containing a semicolon. The test ClickHouse migration runner splits statements on semicolons, so it cut the CREATE TABLE mid-statement and tried to parse the comment tail as SQL, which failed ClickHouse setup for every test suite that starts a container. Removing the comment leaves the CREATE as a single statement. --- .../clickhouse/schema/034_create_webhook_deliveries_v1.sql | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql b/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql index 862217db382..40730b55923 100644 --- a/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql +++ b/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql @@ -27,7 +27,5 @@ CREATE TABLE IF NOT EXISTS trigger_dev.webhook_deliveries_v1 ENGINE = ReplacingMergeTree(_version, _is_deleted) PARTITION BY toYYYYMM(created_at) ORDER BY (organization_id, project_id, environment_id, created_at, delivery_id) --- 60d retention aligned with the Postgres partition retention. ttl_only_drop_parts makes expiry a --- whole-part drop (monthly partitions); materialize_ttl_recalculate_only keeps TTL lightweight. TTL toDateTime(created_at) + INTERVAL 60 DAY SETTINGS ttl_only_drop_parts = 1, materialize_ttl_recalculate_only = 1; From be7ef5aa1182d009e3954ee9867279f130aa86fe Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 15:53:52 +0100 Subject: [PATCH 6/9] fix(webapp,webhook-sources): fix the production build and the internal test shard The webhook API action routes exported action/loader via an inline destructured `export const { action, loader } = createActionApiRoute(...)`, which the Remix production build cannot strip, so it pulled the server-only ~/db.server into the client bundle and failed the build. Split the export into a plain const plus a separate `export { action, loader }`, matching the other API routes, so the server code is removed as expected. Also drop the extra --run from the @internal/webhook-sources test script; CI appends its own --run and the duplicate flag failed the shard. --- .../routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts | 4 +++- .../routes/api.v1.webhooks.endpoints.$endpointId.disable.ts | 4 +++- .../routes/api.v1.webhooks.endpoints.$endpointId.enable.ts | 4 +++- .../api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts | 4 +++- internal-packages/webhook-sources/package.json | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts b/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts index 0b375acb4d2..04f1a409c0d 100644 --- a/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts +++ b/apps/webapp/app/routes/api.v1.webhooks.deliveries.$deliveryId.replay.ts @@ -10,7 +10,7 @@ const ParamsSchema = z.object({ deliveryId: z.string() }); // POST /api/v1/webhooks/deliveries/:deliveryId/replay — re-run the delivery's task from its stored // event as a NEW delivery (we don't keep the raw body, so this re-triggers rather than re-verifies). -export const { action, loader } = createActionApiRoute( +const { action, loader } = createActionApiRoute( { params: ParamsSchema, method: "POST", @@ -56,3 +56,5 @@ export const { action, loader } = createActionApiRoute( } } ); + +export { action, loader }; diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts index 545f434122e..4e7eddc3c06 100644 --- a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.disable.ts @@ -8,7 +8,7 @@ import { webhookEngine } from "~/v3/webhookEngine.server"; const ParamsSchema = z.object({ endpointId: z.string() }); // POST /api/v1/webhooks/endpoints/:endpointId/disable — pause an endpoint (ingress returns 404). -export const { action, loader } = createActionApiRoute( +const { action, loader } = createActionApiRoute( { params: ParamsSchema, method: "POST", @@ -32,3 +32,5 @@ export const { action, loader } = createActionApiRoute( return json(await findWebhookEndpointResource(authentication, params.endpointId)); } ); + +export { action, loader }; diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts index 562a3a80fb6..d1ac19a4324 100644 --- a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.enable.ts @@ -8,7 +8,7 @@ import { webhookEngine } from "~/v3/webhookEngine.server"; const ParamsSchema = z.object({ endpointId: z.string() }); // POST /api/v1/webhooks/endpoints/:endpointId/enable — resume a paused endpoint. -export const { action, loader } = createActionApiRoute( +const { action, loader } = createActionApiRoute( { params: ParamsSchema, method: "POST", @@ -32,3 +32,5 @@ export const { action, loader } = createActionApiRoute( return json(await findWebhookEndpointResource(authentication, params.endpointId)); } ); + +export { action, loader }; diff --git a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts index caaf9c6b7a6..52038195983 100644 --- a/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts +++ b/apps/webapp/app/routes/api.v1.webhooks.endpoints.$endpointId.rotate-secret.ts @@ -11,7 +11,7 @@ const ParamsSchema = z.object({ endpointId: z.string() }); // POST /api/v1/webhooks/endpoints/:endpointId/rotate-secret — mint a new signing secret and return // it ONCE. Only for schemes we generate (hmac / shared-secret); asymmetric endpoints set a public key. -export const { action, loader } = createActionApiRoute( +const { action, loader } = createActionApiRoute( { params: ParamsSchema, method: "POST", @@ -49,3 +49,5 @@ export const { action, loader } = createActionApiRoute( return json({ id: endpoint.friendlyId, secretSet: true as const, secret }); } ); + +export { action, loader }; diff --git a/internal-packages/webhook-sources/package.json b/internal-packages/webhook-sources/package.json index 09c6ae243a4..fcf013e6998 100644 --- a/internal-packages/webhook-sources/package.json +++ b/internal-packages/webhook-sources/package.json @@ -14,7 +14,7 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "test": "vitest --run", + "test": "vitest", "ingest": "tsx src/ingest.ts", "catalog:status": "tsx catalog/status.ts", "catalog:aggregate": "tsx catalog/build-aggregates.ts" From 02bc5327dd70e7c22c66b59fc79faf3529e986e1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 16:04:18 +0100 Subject: [PATCH 7/9] fix(slack): align vitest with the repo version so blob reports merge The slack package pinned its own vitest ^2.1.9 while the rest of the repo uses 4.1.7, so its test blob reports could not be merged with the others (the packages Merge Reports step requires a single vitest version). Drop the pin so slack inherits the root vitest like every other package. --- packages/slack/package.json | 3 +- pnpm-lock.yaml | 231 ------------------------------------ 2 files changed, 1 insertion(+), 233 deletions(-) diff --git a/packages/slack/package.json b/packages/slack/package.json index db739e2e216..fa8b483eaa5 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -51,8 +51,7 @@ "@trigger.dev/sdk": "workspace:4.5.0-rc.7", "rimraf": "6.0.1", "tshy": "^3.0.2", - "tsx": "4.17.0", - "vitest": "^2.1.9" + "tsx": "4.17.0" }, "engines": { "node": ">=18.20.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a4f643faaf..0c3633b2d55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2160,9 +2160,6 @@ importers: tsx: specifier: 4.17.0 version: 4.17.0 - vitest: - specifier: ^2.1.9 - version: 2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) packages/trigger-sdk: dependencies: @@ -8246,23 +8243,9 @@ packages: '@vitest/browser': optional: true - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@vitest/expect@4.1.7': resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.4.2 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - '@vitest/mocker@4.1.7': resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: @@ -8274,33 +8257,18 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@vitest/pretty-format@4.1.7': resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - '@vitest/runner@4.1.7': resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@vitest/snapshot@4.1.7': resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - '@vitest/spy@4.1.7': resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@vitest/utils@4.1.7': resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} @@ -8938,10 +8906,6 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -8981,10 +8945,6 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - cheminfo-types@1.8.1: resolution: {integrity: sha512-FRcpVkox+cRovffgqNdDFQ1eUav+i/Vq/CUd1hcfEl2bevntFlzznL+jE8g4twl6ElB7gZjCko6pYpXyMn+6dA==} @@ -9663,10 +9623,6 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -11792,9 +11748,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lowercase-keys@1.0.1: resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} engines: {node: '>=0.10.0'} @@ -12954,10 +12907,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -14723,26 +14672,14 @@ packages: tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} - engines: {node: '>=14.0.0'} - tldts-core@7.0.7: resolution: {integrity: sha512-ECqb8imSroX1UmUuhRBNPkkmtZ8mHEenieim80UVxG0M5wXVjY2Fp2tYXCPvk+nLy1geOhFpeD5YQhM/gF63Jg==} @@ -15327,11 +15264,6 @@ packages: engines: {node: '>=v14.16.0'} hasBin: true - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - vite-node@3.1.4: resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -15416,31 +15348,6 @@ packages: yaml: optional: true - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': 24.13.3 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vitest@4.1.7: resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -23103,13 +23010,6 @@ snapshots: tinyrainbow: 3.1.0 vitest: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.7)(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0)) - '@vitest/expect@2.1.9': - dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - tinyrainbow: 1.2.0 - '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 @@ -23119,14 +23019,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@2.1.9(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 2.1.9 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) - '@vitest/mocker@4.1.7(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.7 @@ -23143,30 +23035,15 @@ snapshots: optionalDependencies: vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.22.4)(yaml@2.9.0) - '@vitest/pretty-format@2.1.9': - dependencies: - tinyrainbow: 1.2.0 - '@vitest/pretty-format@4.1.7': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@2.1.9': - dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 - '@vitest/runner@4.1.7': dependencies: '@vitest/utils': 4.1.7 pathe: 2.0.3 - '@vitest/snapshot@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.21 - pathe: 1.1.2 - '@vitest/snapshot@4.1.7': dependencies: '@vitest/pretty-format': 4.1.7 @@ -23174,18 +23051,8 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 - '@vitest/spy@4.1.7': {} - '@vitest/utils@2.1.9': - dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 - '@vitest/utils@4.1.7': dependencies: '@vitest/pretty-format': 4.1.7 @@ -23927,14 +23794,6 @@ snapshots: ccount@2.0.1: {} - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - chai@6.2.2: {} chalk@2.4.2: @@ -23964,8 +23823,6 @@ snapshots: chardet@0.7.0: {} - check-error@2.1.3: {} - cheminfo-types@1.8.1: {} chevrotain-allstar@0.4.1(chevrotain@12.0.0): @@ -24670,8 +24527,6 @@ snapshots: mimic-response: 3.1.0 optional: true - deep-eql@5.0.2: {} - deep-extend@0.6.0: {} deep-object-diff@1.1.9: {} @@ -27094,8 +26949,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.2.1: {} - lowercase-keys@1.0.1: {} lowercase-keys@2.0.0: {} @@ -28716,8 +28569,6 @@ snapshots: pathe@2.0.3: {} - pathval@2.0.1: {} - peberminta@0.9.0: {} peek-readable@5.4.2: {} @@ -30807,16 +30658,10 @@ snapshots: '@types/tinycolor2': 1.4.3 tinycolor2: 1.6.0 - tinypool@1.1.1: {} - tinypool@2.1.0: {} - tinyrainbow@1.2.0: {} - tinyrainbow@3.1.0: {} - tinyspy@3.0.2: {} - tldts-core@7.0.7: {} tldts@7.0.7: @@ -31451,27 +31296,6 @@ snapshots: - supports-color - terser - vite-node@2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3(supports-color@10.0.0) - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vite-node@3.1.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0): dependencies: cac: 6.7.14 @@ -31541,23 +31365,6 @@ snapshots: tsx: 3.12.2 yaml: 2.9.0 - vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.15 - rollup: 4.60.1 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 24.13.3 - fsevents: 2.3.3 - jiti: 2.7.0 - lightningcss: 1.32.0 - terser: 5.46.1 - tsx: 4.17.0 - yaml: 2.9.0 - vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.20.6)(yaml@2.9.0): dependencies: esbuild: 0.25.12 @@ -31592,44 +31399,6 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vitest@2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0): - dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - debug: 4.4.3(supports-color@10.0.0) - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 1.1.2 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 1.2.0 - vite: 6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) - vite-node: 2.1.9(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.17.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.13.3 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.7)(vite@6.4.2(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.1)(tsx@3.12.2)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.7 From 0bf716b8da70e6af11cb512099ccb20983c0b47e Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 16:04:19 +0100 Subject: [PATCH 8/9] chore: release hosted webhooks as a minor version New functionality (hosted webhooks, agent channels, human-in-the-loop), so this ships as a minor bump, not a patch. --- .changeset/hosted-webhook-ingress.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/hosted-webhook-ingress.md b/.changeset/hosted-webhook-ingress.md index 62363d2b835..82e5f06fd5b 100644 --- a/.changeset/hosted-webhook-ingress.md +++ b/.changeset/hosted-webhook-ingress.md @@ -1,8 +1,8 @@ --- -"@trigger.dev/core": patch -"@trigger.dev/sdk": patch -"@trigger.dev/slack": patch -"trigger.dev": patch +"@trigger.dev/core": minor +"@trigger.dev/sdk": minor +"@trigger.dev/slack": minor +"trigger.dev": minor --- Add hosted webhooks: receive and verify provider webhooks as a task, with no ingress or verification code of your own. From 0375d3399f7ebd4479b6b28e9ddd7da945161966 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 23 Jul 2026 23:20:34 +0100 Subject: [PATCH 9/9] fix(clickhouse): renumber the webhook deliveries migration to 036 main added 034_add_is_test_to_sessions_v1 and 035_fix_error_display_derivation while this branch was open, so the branch's 034_create_webhook_deliveries_v1 both duplicated the 034 number and sorted below the applied 035. Goose in strict mode rejects a version below current, which would block the deploy. Renumber to 036 (the table name is unchanged). --- ...ook_deliveries_v1.sql => 036_create_webhook_deliveries_v1.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal-packages/clickhouse/schema/{034_create_webhook_deliveries_v1.sql => 036_create_webhook_deliveries_v1.sql} (100%) diff --git a/internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql b/internal-packages/clickhouse/schema/036_create_webhook_deliveries_v1.sql similarity index 100% rename from internal-packages/clickhouse/schema/034_create_webhook_deliveries_v1.sql rename to internal-packages/clickhouse/schema/036_create_webhook_deliveries_v1.sql