Skip to content

Webhooks

Chronicle.WebHooks provides an idiomatic Elixir API for registering and inspecting Chronicle webhooks.

Webhooks let Chronicle push observed events to external HTTP endpoints.

Discoverable webhooks follow the same pattern as projections and constraints: define them as modules and Chronicle exposes metadata through __chronicle_webhook__/1.

defmodule MyApp.WebHooks.AccountEvents do
use Chronicle.WebHooks.Webhook,
id: "account-events",
target_url: "https://example.com/chronicle/webhooks",
event_sequence_id: "event-log"
alias Chronicle.WebHooks.DefinitionBuilder
@impl true
def define(builder) do
builder
|> DefinitionBuilder.with_event_type(MyApp.Events.AccountOpened)
|> DefinitionBuilder.with_event_type(MyApp.Events.FundsDeposited)
|> DefinitionBuilder.with_header("x-source", "my-app")
|> DefinitionBuilder.with_bearer_token(System.fetch_env!("WEBHOOK_TOKEN"))
end
end

When Chronicle.Client starts with otp_app: :my_app, discoverable webhooks are found automatically and registered on startup.

{Chronicle.Client,
connection_string: "chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000",
event_store: "banking",
otp_app: :my_app}

You can also register explicit modules:

{Chronicle.Client,
connection_string: "chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000",
event_store: "banking",
webhooks: [MyApp.WebHooks.AccountEvents]}

Register a webhook without defining a module:

:ok =
Chronicle.WebHooks.register(
"account-events",
"https://example.com/chronicle/webhooks",
fn builder ->
builder
|> Chronicle.WebHooks.DefinitionBuilder.with_event_type(MyApp.Events.AccountOpened)
|> Chronicle.WebHooks.DefinitionBuilder.with_basic_auth("chronicle", System.fetch_env!("WEBHOOK_PASSWORD"))
|> Chronicle.WebHooks.DefinitionBuilder.with_header("x-source", "my-app")
end
)

Chronicle.WebHooks.DefinitionBuilder is immutable and pipeline-friendly.

builder
|> DefinitionBuilder.on_event_sequence("audit-sequence")
|> DefinitionBuilder.with_event_type(MyApp.Events.AccountOpened)
|> DefinitionBuilder.with_basic_auth("user", "password")
|> DefinitionBuilder.with_bearer_token("token")
|> DefinitionBuilder.with_oauth("https://login.example.com", "client-id", "client-secret")
|> DefinitionBuilder.with_header("x-source", "my-app")
|> DefinitionBuilder.not_replayable()
|> DefinitionBuilder.not_active()

If you don’t select any event types, the webhook uses the event types the client was given in :event_types or discovered in your application. When that list is empty, for example with discover: false and no :event_types, it falls back to every event type module loaded in the VM. Select event types explicitly to control exactly what the webhook receives.

not_replayable() and not_active() need Chronicle kernel 19.33.0 or later. An older kernel ignores these flags and treats the webhook as replayable and active regardless.

{:ok, webhooks} = Chronicle.WebHooks.all()
:ok = Chronicle.WebHooks.remove("account-events")

Registered webhooks are returned as %Chronicle.WebHooks.Definition{} with:

  • :id
  • :event_sequence_id
  • :event_types
  • :target
  • :replayable?
  • :active?

The nested %Chronicle.WebHooks.Target{} contains:

  • :url
  • :headers
  • :authorization

When you build a definition locally, authorization is one of:

  • {:basic, %{username: ..., password: ...}}
  • {:bearer, %{token: ...}}
  • {:oauth, %{authority: ..., client_id: ..., client_secret: ...}}
  • nil

Chronicle doesn’t return credentials when you read webhooks back, so :authorization is always nil on definitions from Chronicle.WebHooks.all/1.

webhooks = Chronicle.WebHooks.discover()
:ok = Chronicle.WebHooks.register_discovered()

Chronicle also delegates to the webhook API:

{:ok, webhooks} = Chronicle.get_webhooks()
:ok = Chronicle.register_discovered_webhooks()
:ok = Chronicle.remove_webhook("account-events")