Accrue.Config (accrue v1.3.0)

Copy Markdown View Source

Runtime configuration schema for Accrue, backed by NimbleOptions.

This module is the single source of truth for supported :accrue application keys. Host code reads validated values via get!/1 or Application.get_env/3; extend behaviour through adapters, not by editing this schema from application code.

Compile-time vs runtime

Adapter atoms (:processor, :mailer, :mailer_adapter, :invoice_pdf_adapter, :pdf_adapter, :auth_adapter) are stable per-deploy and fine at compile time via Application.compile_env!/2.

Secrets (:stripe_secret_key) and host-owned fields (:default_currency, :from_email, brand colors) MUST be read at runtime. See CLAUDE.md §Config Boundaries.

Options

  • :repo (atom/0) - Required. Host Ecto.Repo module that Accrue writes to (event ledger, webhook events, billing tables).

  • :processor (atom/0) - Processor adapter implementing Accrue.Processor behaviour. The default value is Accrue.Processor.Fake.

  • :mailer (atom/0) - Mailer pipeline module implementing Accrue.Mailer behaviour. The default value is Accrue.Mailer.Default.

  • :mailer_adapter (atom/0) - Swoosh-backed mailer delivery module. The default value is Accrue.Mailer.Swoosh.

  • :invoice_pdf_adapter (atom/0) - Invoice PDF adapter implementing Accrue.InvoiceRenderer behaviour. Defaults to the native Rendro-backed invoice renderer. The default value is Accrue.InvoiceRenderer.Rendro.

  • :pdf_adapter (atom/0) - Legacy HTML-to-PDF adapter implementing Accrue.PDF behaviour. Used by the Chromic invoice renderer path and by custom HTML callers. The default value is Accrue.PDF.ChromicPDF.

  • :auth_adapter (atom/0) - Auth adapter implementing Accrue.Auth behaviour. The default value is Accrue.Auth.Default.

  • :plan_resolver - Optional host-owned resolver module implementing Accrue.PlanResolver. Required when a processor needs more than a bare price_id to perform plan changes through the shared facade (for example Braintree swap-plan flows). The default value is nil.

  • :storage_adapter (atom/0) - Storage adapter implementing Accrue.Storage behaviour. v1.0 ships Accrue.Storage.Null only; hosts supply a custom adapter (e.g., S3) to enable persisted asset storage. Accrue.Storage.Filesystem ships in v1.1. The default value is Accrue.Storage.Null.

  • :stripe_secret_key (String.t/0) - Runtime Stripe secret key. MUST be read at runtime only; never via Application.compile_env!/2. Validated at boot when processor == Accrue.Processor.Stripe.

  • :stripe_api_version (String.t/0) - Stripe API version pinned by the :lattice_stripe wrapper. The default value is "2026-03-25.dahlia".

  • :emails (keyword/0) - Per-email-type switches. Keys are email type atoms; values are boolean or {Mod, :fun, args} MFA callbacks. The default value is [].

  • :email_overrides (keyword/0) - Per-email-type template module overrides (third rung of the override ladder; see guides/email.md). Keys are email type atoms; values are module names. The default value is [].

  • :attach_invoice_pdf (boolean/0) - Auto-attach invoice PDF to the receipt email. The default value is true.

  • :enforce_immutability (boolean/0) - When true, Accrue.Application boot raises if the current PG role has UPDATE/DELETE on accrue_events. The default value is false.

  • :business_name (String.t/0) - Business name shown in email headers, PDFs, and admin UI. The default value is "Accrue".

  • :business_address (String.t/0) - Business postal address shown in invoice footers. The default value is "".

  • :logo_url (String.t/0) - Absolute URL to the brand logo used in email + PDF headers. The default value is "".

  • :support_email (String.t/0) - Reply-to support email address for transactional mail. The default value is "support@example.com".

  • :from_email (String.t/0) - Default From: address for transactional mail. The default value is "noreply@example.com".

  • :from_name (String.t/0) - Default From: name for transactional mail. The default value is "Accrue".

  • :default_currency (atom/0) - Default currency when one is not explicitly supplied. The default value is :usd.

  • :webhook_signing_secrets (term/0) - Map of processor atom to signing secret(s). Each value is a string or list of strings for rotation. Example: %{stripe: ["whsec_old", "whsec_new"]}. The default value is %{}.

  • :succeeded_retention_days - Number of days to retain :succeeded webhook events before the Pruner deletes them. Set to :infinity to disable pruning. Default: 14. The default value is 14.

  • :dead_retention_days - Number of days to retain :dead webhook events before the Pruner deletes them. Set to :infinity to disable pruning. Default: 90. The default value is 90.

  • :webhook_handlers (list of atom/0) - List of modules implementing Accrue.Webhook.Handler behaviour. Called sequentially after the default handler on each webhook event. Example: [MyApp.BillingHandler, MyApp.AnalyticsHandler]. The default value is [].

  • :portal_mount_path (String.t/0) - Mount path for the first-party Accrue.Portal routes when a processor uses local portal semantics. Returned checkout / billing-portal URLs append to this normalized path after :portal_base_url supplies the absolute host. The default value is "/billing".

  • :portal_base_url - Absolute base URL (for example https://app.example.com) used to generate returned local portal checkout and billing-portal URLs. Leave unset only when those local portal URLs are not in use; portal_url/1 raises instead of falling back to a relative path. The default value is nil.

  • :braintree_client_token_generator (atom/0) - Module used to generate Braintree client tokens for Hosted Fields. Must export generate/1. The default value is Braintree.ClientToken.

  • :expiring_card_thresholds - Strictly-descending list of day thresholds at which the expiring-card reminder email fires ahead of a stored card's expiration. Default: [30, 7, 1] — 30, 7, and 1 days out. The default value is [30, 7, 1].

  • :idempotency_mode - How Accrue.Actor.current_operation_id!/0 behaves when the process dict has no operation_id. :strict raises Accrue.ConfigError; :warn (the default) generates a random UUID and logs a warning. Set to :strict in production to ensure every outbound processor call carries a deterministic idempotency key. The default value is :warn.

  • :succeeded_refund_retention_days (pos_integer/0) - Number of days to retain :succeeded refund records before pruning Default: 90. The default value is 90.

  • :dunning (keyword/0) - Dunning grace-period overlay config. :mode is :stripe_smart_retries or :disabled; :terminal_action is :unpaid or :canceled; :grace_days adds N days past Stripe's last retry before Accrue asks the processor facade to move the subscription to the terminal action. :campaign is the multi-step dunning cadence (see its own doc). The default value is [mode: :stripe_smart_retries, grace_days: 14, terminal_action: :unpaid, telemetry_prefix: [:accrue, :ops], campaign: [enabled: true, steps: [[after_days: 0, key: :reminder, template: Accrue.Emails.InvoicePaymentFailed], [after_days: 5, key: :action_required, template: Accrue.Emails.DunningActionRequired], [after_days: 12, key: :final_notice, template: Accrue.Emails.DunningFinalNotice]]], engine: Accrue.Dunning.Engine.Oban].

    • :mode - The default value is :stripe_smart_retries.

    • :grace_days (pos_integer/0) - The default value is 14.

    • :terminal_action - The default value is :unpaid.

    • :telemetry_prefix (list of atom/0) - The default value is [:accrue, :ops].

    • :campaign - Multi-step dunning cadence (D-04). A keyword list of [enabled: boolean, steps: [...]] where each step is [after_days: non_neg_integer, key: atom, template: atom]. after_days is ABSOLUTE from campaign start, strictly increasing and unique across the list; key is required and unique (it becomes the Oban-unique step identity). Ships a default journey ON by default (offsets [0, 5, 12]); set campaign: false (normalized to [enabled: false, steps: []]) to opt out. steps: [] while enabled: true is a loud error. The last step's after_days MUST be <= grace_days (validated loud at boot) so the final notice precedes the sweeper's terminal action. The default value is [enabled: true, steps: [[after_days: 0, key: :reminder, template: Accrue.Emails.InvoicePaymentFailed], [after_days: 5, key: :action_required, template: Accrue.Emails.DunningActionRequired], [after_days: 12, key: :final_notice, template: Accrue.Emails.DunningFinalNotice]]].

    • :engine (atom/0) - Module implementing Accrue.Dunning.Engine. Default: Accrue.Dunning.Engine.Oban (built-in Oban campaign). Set to Accrue.Integrations.Chimeway to delegate orchestration to Chimeway. The default value is Accrue.Dunning.Engine.Oban.

  • :webhook_endpoints (keyword/0) - Map of endpoint name to [secret:, mode:] for multi-endpoint webhooks. Example: [primary: [secret: "whsec_..."], connect: [secret: "whsec_...", mode: :connect]]. The default value is [].

  • :dlq_replay_batch_size (pos_integer/0) - Number of rows per chunk in Accrue.Webhooks.DLQ.requeue_where/2 bulk replay. The default value is 100.

  • :dlq_replay_stagger_ms (non_neg_integer/0) - Milliseconds to sleep between chunks during DLQ bulk replay (protects downstream). Default: 1_000. The default value is 1000.

  • :dlq_replay_max_rows (pos_integer/0) - Hard cap on bulk replay. Returns {:error, :replay_too_large} unless force: true is passed. Default: 10_000. The default value is 10000.

  • :branding (keyword/0) - Branding config. Single source of truth for email + PDF brand. :from_email and :support_email are required for any real deploy. See guides/branding.md. The default value is [].

    • :business_name (String.t/0) - The default value is "Accrue".

    • :from_name (String.t/0) - The default value is "Accrue".

    • :from_email (String.t/0) - Required.

    • :support_email (String.t/0) - Required.

    • :reply_to_email - The default value is nil.

    • :logo_url - The default value is nil.

    • :logo_dark_url - The default value is nil.

    • :accent_color - The default value is "#1F6FEB".

    • :secondary_color - The default value is "#6B7280".

    • :font_stack (String.t/0) - The default value is "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif".

    • :company_address - The default value is nil.

    • :support_url - The default value is nil.

    • :social_links (keyword/0) - The default value is [].

    • :list_unsubscribe_url - The default value is nil.

  • :default_locale (String.t/0) - Application-wide default locale for email + PDF rendering. Third rung of the locale precedence ladder (after assigns[:locale] and customer.preferred_locale). Bad locales fall back to "en". The default value is "en".

  • :default_timezone (String.t/0) - Application-wide default IANA timezone for datetime rendering. Third rung of the timezone precedence ladder (after assigns[:timezone] and customer.preferred_timezone). Bad zones fall back to "Etc/UTC". The default value is "Etc/UTC".

  • :cldr_backend (atom/0) - Cldr backend module used by Accrue.Workers.Mailer.enrich/2 to validate locale strings. Defaults to Accrue.Cldr. The default value is Accrue.Cldr.

  • :connect (keyword/0) - Stripe Connect configuration. :default_stripe_account is the fallback connected account id used when no per-call override or pdict scope is active (three-level precedence chain). :platform_fee configures the default flat-rate fee consumed by Accrue.Connect.platform_fee/2: :percent is a Decimal percentage (e.g. Decimal.new("2.9") for 2.9%), :fixed is an Accrue.Money fee in minor units added after the percentage, and :min/:max optionally clamp the result. The default value is [default_stripe_account: nil, platform_fee: [percent: Decimal.new("2.9"), fixed: nil, min: nil, max: nil]].

  • :entitlements (keyword/0) - Plan->feature/quota entitlement catalog (host-owned, runtime). Boot-validated and the same price_id may not map to two plans (raises Accrue.ConfigError at boot). See guides/entitlements.md. The default value is [].

    • :plans (keyword/0) - Logical plan name (atom) -> entitlement entry (features/limits/price_ids) The default value is [].

      • :features (list of atom/0) - The default value is [].

      • :limits (keyword/0) - The default value is [].

      • :price_ids (list of String.t/0) - The default value is [].

    • :resolver (atom/0) - Resolver module (Accrue.Entitlements.Resolver behaviour). Default LocalMap. The default value is Accrue.Entitlements.Resolver.LocalMap.

    • :unmapped_action - Behaviour when an active price_id is unmapped. :deny fails closed; never silent-allow. The default value is :deny.

    • :billable - Global billable resolver: a 1-arity fn (conn | socket -> billable | nil). When unset, the default probes current_scope.user -> current_user -> nil. Must never raise — fail closed (resolve to nil) on any miss. The default value is nil.

    • :on_deny - Global deny handler. Default is a content-negotiated opaque 403. Accepts :forbidden | {:redirect, path} | {status, body} | fun/2 | {m,f,a}. A per-guard on_deny: opt overrides this; this overrides the built-in 403. A malformed value fails loud at boot (never silently fails open). The default value is :forbidden.

    • :deny_path (String.t/0) - LiveView fallback redirect target for :forbidden / non-redirectable denies. The default value is "/".

    • :past_due_grace - Entitlement access for :past_due subscriptions. :none (default) fails closed immediately. :dunning honors the dunning grace window (reuses Accrue.Config.dunning()[:grace_days]). A positive integer N honors an entitlement-specific N-day window. Grace grants are affirmative, resolved, configured decisions — never a fail-open. The default value is :none.

    • :stripe_native_sync - Optional Stripe-native entitlement-summary sync. :disabled (default) is fully inert — no webhook reducer runs, no cache table is read or written. :advisory records Stripe entitlement summaries to an advisory cache for audit / telemetry / the admin read-seam; it does NOT change entitled?/has_active_plan? decisions in v1.x (local mapping stays canonical). The enum (not a boolean) reserves room for future additive modes without a breaking config change. The default value is :disabled.

Summary

Functions

Returns the branding config keyword list.

Returns a single branding key. Raises if the key is unknown.

Returns the configured Cldr backend module used by Accrue.Workers.Mailer.enrich/2 to validate locale strings.

Returns the Connect config keyword list.

Returns the number of days to retain :dead webhook events.

Returns the application default locale string.

Returns the application default IANA timezone string.

Returns the DLQ bulk-replay chunk size.

Returns the hard cap on DLQ bulk-replay rows.

Returns the DLQ bulk-replay inter-chunk stagger in milliseconds.

Returns the dunning grace-period overlay config.

Returns the dunning campaign cadence config (D-07).

Returns whether the multi-step dunning campaign is enabled (D-07).

Returns the configured dunning campaign steps (D-07).

Returns the configured dunning engine module (D-03).

Returns the runtime :entitlements config keyword list (the plan->feature/ quota catalog), or the schema default [] when unset.

Reads a config key from Application.get_env/3, falling back to the schema default. Raises Accrue.ConfigError if the key is not in the schema at all (prevents silent typos in downstream code).

Returns the past-due entitlement grace policy: :none (default, fail-closed), :dunning (reuse the dunning grace window), or a positive integer of days.

Returns the NimbleOptions schema keyword list. Used by boot-time validation to iterate keys.

Returns the configured Stripe API version string.

Returns the optional Stripe-native entitlement-sync mode: :disabled (default, fully inert) or :advisory (record summaries to the advisory cache for audit / telemetry / the admin read-seam — does NOT change entitled?/has_active_plan? decisions; local mapping stays canonical).

Ergonomic predicate for the Stripe-native sync gate: true when sync is enabled in any non-:disabled mode, false otherwise. The webhook reducer dispatch checks this FIRST so the off lane early-returns before any Repo call.

Returns the number of days to retain :succeeded webhook events.

Validates a keyword list against the Accrue config schema and returns the normalized form. Raises NimbleOptions.ValidationError on failure.

Reads the current :accrue application env at boot time, filters it to the schema-known keys, and validates via NimbleOptions.validate!/2.

NimbleOptions :custom validator for :expiring_card_thresholds.

NimbleOptions :custom validator for the :dunning.campaign cadence (DUN-01, D-04/D-05).

NimbleOptions :custom validator for :branding.accent_color / :branding.secondary_color. Accepts #rgb, #rrggbb, and #rrggbbaa hex color strings; rejects anything else.

NimbleOptions :custom validator for the global :entitlements on_deny handler (Phase 124, ENT-06).

Returns the multi-endpoint webhook config.

Returns the list of user-registered webhook handler modules.

Returns the signing secret(s) for the given processor.

Functions

branding()

@spec branding() :: keyword()

Returns the branding config keyword list.

Reads the nested :branding keyword list from app env, merging any unset keys with their schema defaults so callers can Keyword.fetch! any valid key without re-running the full NimbleOptions validator on every call site.

branding(key)

@spec branding(atom()) :: term()

Returns a single branding key. Raises if the key is unknown.

cldr_backend()

@spec cldr_backend() :: module()

Returns the configured Cldr backend module used by Accrue.Workers.Mailer.enrich/2 to validate locale strings.

connect()

@spec connect() :: keyword()

Returns the Connect config keyword list.

Shape: `[default_stripe_account: String.t() | nil,

      platform_fee: [percent: Decimal.t(), fixed: Accrue.Money.t() | nil,
                     min: Accrue.Money.t() | nil, max: Accrue.Money.t() | nil]]`.

dead_retention_days()

@spec dead_retention_days() :: pos_integer() | :infinity

Returns the number of days to retain :dead webhook events.

default_locale()

@spec default_locale() :: String.t()

Returns the application default locale string.

default_timezone()

@spec default_timezone() :: String.t()

Returns the application default IANA timezone string.

dlq_replay_batch_size()

@spec dlq_replay_batch_size() :: pos_integer()

Returns the DLQ bulk-replay chunk size.

dlq_replay_max_rows()

@spec dlq_replay_max_rows() :: pos_integer()

Returns the hard cap on DLQ bulk-replay rows.

dlq_replay_stagger_ms()

@spec dlq_replay_stagger_ms() :: non_neg_integer()

Returns the DLQ bulk-replay inter-chunk stagger in milliseconds.

dunning()

@spec dunning() :: keyword()

Returns the dunning grace-period overlay config.

dunning_campaign()

@spec dunning_campaign() :: keyword()

Returns the dunning campaign cadence config (D-07).

Raw read with its own nested default — dunning/0 does not normalize the nested :campaign key (identical constraint to past_due_grace/0), so the default journey is supplied here when a host overrides :dunning without re-stating :campaign. Ships ON by default.

dunning_campaign_enabled?()

@spec dunning_campaign_enabled?() :: boolean()

Returns whether the multi-step dunning campaign is enabled (D-07).

false when the campaign is opted out (campaign: false / enabled: false). Fail-closed: an absent :enabled key reads as false.

dunning_campaign_steps()

@spec dunning_campaign_steps() :: [keyword()]

Returns the configured dunning campaign steps (D-07).

Returns [] when the campaign is disabled (so downstream consumers never fire steps for an opted-out host), else the configured step list.

dunning_engine()

@spec dunning_engine() :: module()

Returns the configured dunning engine module (D-03).

Defaults to Accrue.Dunning.Engine.Oban (built-in Oban campaign). Set dunning: [engine: Accrue.Integrations.Chimeway] to opt into Chimeway orchestration.

The atom is returned as-is without loading the module — resolution happens at dispatch time in the calling code.

entitlements()

@spec entitlements() :: keyword()

Returns the runtime :entitlements config keyword list (the plan->feature/ quota catalog), or the schema default [] when unset.

This is a thin runtime accessor (get!/1, NOT compile_env!) because :entitlements is host-owned catalog data that legitimately differs per environment (e.g. test-mode vs live-mode price_ids), like :branding and :dunning. Unlike branding/0 (which merges schema defaults), this is a raw runtime read — it does NOT apply the nested per-plan defaults (features: [], limits: [], price_ids: []). validate_at_boot!/0 validates the config but discards the normalized result and never writes it back to app env. The resolver tolerates missing nested keys via Keyword.get/3 defaults, so it reads this raw value without re-running the full validator.

get!(key)

@spec get!(atom()) :: term()

Reads a config key from Application.get_env/3, falling back to the schema default. Raises Accrue.ConfigError if the key is not in the schema at all (prevents silent typos in downstream code).

normalize_mount_path(path)

@spec normalize_mount_path(String.t()) :: String.t()

past_due_grace()

@spec past_due_grace() :: :none | :dunning | pos_integer()

Returns the past-due entitlement grace policy: :none (default, fail-closed), :dunning (reuse the dunning grace window), or a positive integer of days.

The :none default is applied here because entitlements/0 does a raw read without normalizing nested :entitlements defaults. The :dunning -> grace_days resolution is done by the resolver when it widens the fold, not by this accessor.

plan_resolver()

@spec plan_resolver() :: module() | nil

portal_base_url()

@spec portal_base_url() :: String.t() | nil

portal_mount_path()

@spec portal_mount_path() :: String.t()

portal_url(path)

@spec portal_url(String.t()) :: String.t()

schema()

@spec schema() :: keyword()

Returns the NimbleOptions schema keyword list. Used by boot-time validation to iterate keys.

stripe_api_version()

@spec stripe_api_version() :: String.t()

Returns the configured Stripe API version string.

stripe_native_sync()

@spec stripe_native_sync() :: :disabled | :advisory

Returns the optional Stripe-native entitlement-sync mode: :disabled (default, fully inert) or :advisory (record summaries to the advisory cache for audit / telemetry / the admin read-seam — does NOT change entitled?/has_active_plan? decisions; local mapping stays canonical).

The :disabled default is supplied here because entitlements/0 does a raw read without normalizing nested :entitlements defaults (identical constraint to past_due_grace/0).

stripe_native_sync?()

@spec stripe_native_sync?() :: boolean()

Ergonomic predicate for the Stripe-native sync gate: true when sync is enabled in any non-:disabled mode, false otherwise. The webhook reducer dispatch checks this FIRST so the off lane early-returns before any Repo call.

succeeded_retention_days()

@spec succeeded_retention_days() :: pos_integer() | :infinity

Returns the number of days to retain :succeeded webhook events.

validate!(opts)

@spec validate!(keyword()) :: keyword()

Validates a keyword list against the Accrue config schema and returns the normalized form. Raises NimbleOptions.ValidationError on failure.

validate_at_boot!()

@spec validate_at_boot!() :: :ok

Reads the current :accrue application env at boot time, filters it to the schema-known keys, and validates via NimbleOptions.validate!/2.

Called by Accrue.Application.start/2 before the supervision tree starts. Raises NimbleOptions.ValidationError on misconfig — fail loud rather than limp into production with silently-broken config.

Only schema-known keys are validated. Extra keys in the :accrue env (e.g., per-module adapter configs like Accrue.Mailer.Swoosh) are ignored here — they belong to their own libraries and would otherwise produce spurious unknown option errors.

validate_descending(list)

@spec validate_descending(term()) :: {:ok, [pos_integer()]} | {:error, String.t()}

NimbleOptions :custom validator for :expiring_card_thresholds.

Accepts a non-empty list of positive integers that is strictly descending (each element strictly less than the previous). Returns {:ok, list} on success, {:error, message} on failure.

validate_dunning_campaign(campaign)

@spec validate_dunning_campaign(term()) :: {:ok, keyword()} | {:error, String.t()}

NimbleOptions :custom validator for the :dunning.campaign cadence (DUN-01, D-04/D-05).

Accepts:

  • false — normalized to [enabled: false, steps: []] (opt-out, D-05).
  • a keyword list [enabled: boolean, steps: [...]] where each step is a keyword list [after_days: non_neg_integer, key: atom, template: atom] validated against @step_schema.

Intra-list invariants (the cross-field last_step.after_days <= grace_days invariant is checked separately at boot by validate_dunning_campaign_grace!/1, since a {:custom} validator cannot read the sibling :grace_days key):

  • after_days strictly increasing AND unique across the list.
  • key unique across the list.
  • when enabled: true, steps MUST be non-empty — steps: [] while enabled is a LOUD {:error, _} (never a silent disable, D-05).

Returns {:ok, normalized_keyword} on success, {:error, message} on failure (so boot validation fails loud rather than silently mis-firing).

validate_hex(full)

@spec validate_hex(term()) :: {:ok, String.t()} | {:error, String.t()}

NimbleOptions :custom validator for :branding.accent_color / :branding.secondary_color. Accepts #rgb, #rrggbb, and #rrggbbaa hex color strings; rejects anything else.

validate_on_deny(value)

@spec validate_on_deny(term()) :: {:ok, term()} | {:error, String.t()}

NimbleOptions :custom validator for the global :entitlements on_deny handler (Phase 124, ENT-06).

Accepts any of the supported deny-handler shapes and returns {:ok, value}; a malformed value returns {:error, message} so boot validation (validate_at_boot!/0) fails loud rather than letting a broken deny path silently fail open (T-124-01).

Supported shapes:

  • :forbidden — built-in opaque 403 / LiveView redirect to deny_path.
  • {:redirect, path} when path is a binary.
  • {status, body} when status is an integer and body is a binary.
  • a 2-arity fun (container, ctx -> result).
  • an MFA {m, f, a} when m/f are atoms and a is a list.

webhook_endpoints()

@spec webhook_endpoints() :: keyword()

Returns the multi-endpoint webhook config.

webhook_handlers()

@spec webhook_handlers() :: [module()]

Returns the list of user-registered webhook handler modules.

webhook_signing_secrets(processor)

@spec webhook_signing_secrets(atom()) :: String.t() | [String.t()]

Returns the signing secret(s) for the given processor.

Looks up webhook_signing_secrets in the :accrue application env and extracts the value for the given processor atom. Returns a list of strings (for multi-secret rotation support). Raises Accrue.ConfigError if no secrets are configured for the processor.