Webhooks for Ash Framework — inbound (receive, verify, dedup → domain events) and outbound (sign, deliver, retry, track).
Attach to a resource:
use Ash.Resource,
extensions: [AshHooks]
webhooks do
# convention-resolves to AshHooks.Provider.ComplyCube
inbound :comply_cube do
secret {:app_env, [:my_app, :complycube_secret]}
# optional: extract a stable event id (payload -> {:ok, id} | :error);
# otherwise the provider identity callback, then raw-body digest, applies
end
# convention-resolves to AshHooks.Provider.HubSpotV3 — the
# vendor-default 300s replay window applies; override it either way
inbound :hub_spot_v3 do
secret {:app_env, [:my_app, :hubspot_client_secret]}
end
outbound :order_paid do
signing_mode :dual
end
endBoth halves are independently consumable: inbound-only consumers pull no queue infrastructure (ADR-0004).
Multi-tenant? Declare Ash attribute multitenancy on the four resources
(subscription, endpoint, both ledgers) and pass :tenant /
ctx[:tenant] to every entry point — every query, write, dedup
identity, sweep, and job arg is tenant-scoped, and a tenant-less call
against a multitenant resource fails closed with
{:error, :tenant_required} before any data access. Single-tenant
apps change nothing. The
adoption checklist
walks the ordered transition (ADR-0011).
webhooks
Webhook declarations — inbound sources and outbound events for this resource.
Nested DSLs
webhooks.inbound
inbound nameDeclares an inbound webhook source — a provider whose deliveries this resource receives, verifies, deduplicates, and handles.
Arguments
| Name | Type | Default | Docs |
|---|---|---|---|
name | atom | The provider name (e.g. :comply_cube). |
Options
| Name | Type | Default | Docs |
|---|---|---|---|
secret | any | The signing-secret source: an {M, f, a} callback, {:app_env, path}, a 0-arity function (app-global), or a 1-arity function (tenant -> {:ok, secret} | {:error, :no_webhook_secret} — resolves the TENANT's secret on multitenant ledgers; arity dispatch on literal fns is unambiguous). A literal binary is rejected at parse time (ADR-0005). Scope: this net catches the secret passed AS the option value; arguments of an MFA source are the consumer's own code. | |
provider | atom | The provider MODULE implementing AshHooks.Provider. When unset, the ingress resolves AshHooks.Provider.<Camelized(name)> and fails closed when that module does not exist or does not implement the behavior. | |
event_id | any | Extractor for the provider's external event id from the decoded payload (payload -> {:ok, id} | :error). This explicit extractor takes precedence over the provider's optional event_identity/1. Without either, the raw request-body digest supplies the identity. A provider callback error fails closed rather than falling back to the digest. | |
replay_window_seconds | pos_integer | Replay-protection window for providers whose scheme carries a trustworthy timestamp (e.g. HubSpot v3). Providers without timestamps (e.g. ComplyCube) MUST leave this unset — the verifier rejects a window whose provider declares no timestamp header (AshHooks.Provider.timestamp_header/1 returns nil). The window value is passed to the provider's verify_signature/3 in the context map for scheme-specific enforcement. |
Introspection
Target: AshHooks.Inbound
webhooks.outbound
outbound nameDeclares an outbound webhook event this resource emits to subscribed endpoints — signed (Standard Webhooks canon by default) and delivered with retry/backoff/dead-letter semantics.
Arguments
| Name | Type | Default | Docs |
|---|---|---|---|
name | atom | The outbound event name (e.g. :order_paid). |
Options
| Name | Type | Default | Docs |
|---|---|---|---|
signing_mode | :legacy | :dual | :standard | :standard | Signature envelope: :standard (SW canon), :dual (SW canon + legacy envelope for migrating receivers), :legacy (ADR-0002). |
subscriptions | atom | The consumer's Subscription resource module (carrying AshHooks.Subscription) this event fans out through — the dispatcher matches its rows against the event type. | |
deliveries | atom | The consumer's OutboundDelivery resource module (carrying AshHooks.OutboundDelivery) the dispatcher writes the durable per-endpoint rows into. |
Introspection
Target: AshHooks.Outbound