{ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Actual-sync Configuration", "description": "Configuration schema for Actual-sync service", "type": "object", "additionalProperties": false, "required": ["servers"], "$defs": { "messageTemplate": { "type": "string", "maxLength": 2000, "description": "A user-configurable notification message template (#257). Handlebars syntax: {{ variable }} plus the eq, default, upper and lower helpers, and the if/unless/each/with block helpers. Validated at startup against the consumer's documented variable list; an unknown name, an unregistered helper (lookup and log included), or literal markup Telegram would reject fails startup with the offending token and line. See docs/NOTIFICATIONS.md, section \"Message templates\"." }, "scheduleAlertTemplates": { "type": "object", "additionalProperties": false, "description": "Overrides for one or more of the missing-payment alert's built-in messages (#258). Any key left out keeps the built-in English wording. See docs/SCHEDULE_ALERTS.md, section \"Templates\".", "properties": { "missing": { "$ref": "#/$defs/messageTemplate" }, "wrongAmount": { "$ref": "#/$defs/messageTemplate" }, "resolved": { "$ref": "#/$defs/messageTemplate" }, "cannotCheck": { "$ref": "#/$defs/messageTemplate" }, "ruleUnmatched": { "$ref": "#/$defs/messageTemplate" }, "muteEnded": { "$ref": "#/$defs/messageTemplate" } } }, "scheduleAlert": { "type": "object", "additionalProperties": false, "description": "One missing-payment alert rule (#258): watches a single schedule (schedule) or every schedule whose name starts with a prefix (schedulePrefix, expanding to one alert per match, all sharing this rule's id).", "oneOf": [ { "required": ["schedule"], "not": { "required": ["schedulePrefix"] } }, { "required": ["schedulePrefix"], "not": { "required": ["schedule"] } } ], "properties": { "id": { "type": "string", "pattern": "^[a-z0-9-]{1,64}$", "description": "Stable alert id, used by the schedule_alerts ledger and the dashboard (#261/#262). Defaults to a slug of schedule/schedulePrefix. Must be unique per server (checked at startup, since a JSON Schema cannot enforce uniqueness across array items)." }, "enabled": { "type": "boolean", "default": true, "description": "Set to false to switch this alert rule off in the config (#261). A rule disabled here cannot be switched on from the dashboard; it shows as Disabled in config." }, "schedule": { "type": "string", "minLength": 1, "description": "Exact name of one Actual schedule to watch." }, "schedulePrefix": { "type": "string", "minLength": 1, "description": "Watch every Actual schedule whose name starts with this prefix (e.g. several rentals sharing a naming convention), each as its own occurrence of this one rule." }, "graceDays": { "type": "integer", "minimum": 0, "maximum": 60, "default": 3, "description": "Days after the occurrence date before it is considered overdue (the deadline is occurrence + graceDays)." }, "earlyDays": { "type": "integer", "minimum": 0, "maximum": 31, "default": 2, "description": "Days before the occurrence date a matching transaction still counts as on time." }, "period": { "type": "string", "enum": ["occurrence", "next"], "default": "occurrence", "description": "What the {{period}} template variable names: the occurrence's own month (occurrence), or the following month (next, for a payment due at month end for the month ahead)." }, "amountTolerancePct": { "type": "number", "minimum": 0, "maximum": 100, "description": "How far a posted amount may differ from the schedule's expected amount and still count as on time, as a percentage. Defaults to the schedule's own amountOp in Actual: 0 for an exact amount, 7.5 for \"approximately\"." }, "remindEveryDays": { "type": "integer", "minimum": 1, "maximum": 30, "description": "Repeat a missing alert every N days while it stays unresolved, instead of sending it once. Off (no repeat) by default." }, "channels": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "enum": ["telegram", "email", "slack", "discord", "webhook", "ntfy"] }, "description": "Notification channels this alert uses. Defaults to every channel currently configured and enabled." }, "templates": { "$ref": "#/$defs/scheduleAlertTemplates" } } }, "scheduleAlertsBlock": { "type": "object", "additionalProperties": false, "required": ["alerts"], "description": "Missing-payment alerts for this server (#258): compares Actual's schedules against posted transactions and notifies when an expected payment is late, missing, wrong-amount, or cannot be checked. See docs/SCHEDULE_ALERTS.md.", "properties": { "digest": { "type": "boolean", "default": false, "description": "Merge every alert event from one sync run for this server into a single message per channel, instead of one message per event." }, "staleAfterDays": { "type": "integer", "minimum": 0, "maximum": 60, "default": 3, "description": "How many days old the account's last successful sync may be before a missing payment is reported as cannotCheck (bank connection likely broken) instead of missing. Strictly greater than this many days counts as stale." }, "dateFormat": { "type": "string", "default": "D MMM YYYY", "description": "moment.js format string used for every date/time template variable." }, "locale": { "type": "string", "default": "en", "description": "Locale tag used when rendering Actual's own cadence wording (the {{cadence}} variable)." }, "templates": { "$ref": "#/$defs/scheduleAlertTemplates" }, "alerts": { "type": "array", "description": "Config-defined rules. May be empty: the block then only switches the feature on for this server, and rules are added from the dashboard (#262).", "items": { "$ref": "#/$defs/scheduleAlert" } } } } }, "properties": { "$schema": { "type": "string", "description": "Optional JSON Schema reference for editor autocomplete/validation. Ignored at runtime." }, "timezone": { "type": "string", "minLength": 1, "description": "IANA timezone name (e.g. \"Europe/Madrid\", \"America/New_York\") used for cron schedules and for every time shown in logs and notifications. Defaults to the process timezone (TZ, else the system zone). Checked at config load.", "examples": ["Europe/Madrid", "America/New_York", "UTC"] }, "servers": { "type": "array", "description": "Array of Actual Budget server configurations", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["name", "url", "password", "syncId", "dataDir"], "properties": { "name": { "type": "string", "description": "Human-readable name for the server", "minLength": 1 }, "url": { "type": "string", "description": "Actual Budget server URL", "pattern": "^https?://.+", "examples": ["http://localhost:5006", "https://actual.example.com"] }, "password": { "type": "string", "description": "Server password", "minLength": 1 }, "syncId": { "type": "string", "description": "Budget sync ID from Actual Budget settings", "minLength": 1 }, "dataDir": { "type": "string", "description": "Local directory for budget file cache", "minLength": 1 }, "encryptionPassword": { "type": "string", "description": "Budget file encryption password (optional, only needed if budget is encrypted with end-to-end encryption)", "minLength": 1 }, "sync": { "type": "object", "additionalProperties": false, "description": "Server-specific sync configuration (overrides global settings)", "properties": { "phaseTimeoutSeconds": { "type": "integer", "description": "Seconds each Actual API call (init, download, load, query, sync, shutdown) may take for this server before the sync fails (#272)", "minimum": 30, "maximum": 3600 }, "maxRetries": { "type": "integer", "description": "Maximum number of retry attempts for this server", "minimum": 0, "maximum": 10 }, "baseRetryDelayMs": { "type": "integer", "description": "Base delay in milliseconds for exponential backoff for this server", "minimum": 1000, "maximum": 10000 }, "schedule": { "type": "string", "description": "Cron expression for sync schedule for this server (5 fields, or 6 with a leading seconds field)", "pattern": "^[0-9A-Za-z*,/?#-]+(\\s+[0-9A-Za-z*,/?#-]+){4,5}$", "examples": ["0 2 * * *", "*/30 * * * *", "0 2 * * MON"] }, "autoRetry": { "type": "object", "additionalProperties": false, "description": "Automatic retry for failed automated syncs for this server (overrides global; does NOT apply to manual syncs)", "properties": { "enabled": { "type": "boolean", "description": "Enable automatic retry for failed automated syncs" }, "maxAttempts": { "type": "integer", "description": "Maximum number of retry attempts after initial failure", "minimum": 0, "maximum": 5 }, "delayMinutes": { "type": "integer", "description": "Delay in minutes between retry attempts", "minimum": 1, "maximum": 1440 } } } } }, "logging": { "type": "object", "additionalProperties": false, "description": "Server-specific logging configuration (overrides global settings)", "properties": { "level": { "type": "string", "description": "Log level for this server (DEBUG, INFO, WARN, ERROR)", "enum": ["DEBUG", "INFO", "WARN", "ERROR", "debug", "info", "warn", "error"] }, "format": { "type": "string", "description": "Deprecated and ignored: the format is set per sink, not per server", "enum": ["pretty", "json"] } } }, "scheduleAlerts": { "$ref": "#/$defs/scheduleAlertsBlock" } } } }, "sync": { "type": "object", "additionalProperties": false, "description": "Global sync behavior configuration (can be overridden per server)", "properties": { "phaseTimeoutSeconds": { "type": "integer", "description": "Seconds each Actual API call (init, download, load, query, sync, shutdown) may take before the sync fails and the queue moves on (#272)", "minimum": 30, "maximum": 3600, "default": 300 }, "maxRetries": { "type": "integer", "description": "Maximum number of retry attempts", "minimum": 0, "maximum": 10, "default": 5 }, "baseRetryDelayMs": { "type": "integer", "description": "Base delay in milliseconds for exponential backoff", "minimum": 1000, "maximum": 10000, "default": 3000 }, "schedule": { "type": "string", "description": "Cron expression for sync schedule (5 fields, or 6 with a leading seconds field)", "pattern": "^[0-9A-Za-z*,/?#-]+(\\s+[0-9A-Za-z*,/?#-]+){4,5}$", "default": "03 03 */2 * *", "examples": ["0 2 * * *", "*/30 * * * *", "0 2 * * MON"] }, "autoRetry": { "type": "object", "additionalProperties": false, "description": "Automatic retry configuration for failed automated syncs (does NOT apply to manual syncs)", "properties": { "enabled": { "type": "boolean", "description": "Enable automatic retry for failed automated syncs", "default": true }, "maxAttempts": { "type": "integer", "description": "Maximum number of retry attempts after initial failure", "minimum": 0, "maximum": 5, "default": 1 }, "delayMinutes": { "type": "integer", "description": "Delay in minutes between retry attempts", "minimum": 1, "maximum": 1440, "default": 31 } } } } }, "logging": { "type": "object", "additionalProperties": false, "description": "Logging configuration", "properties": { "level": { "type": "string", "description": "Global log level (DEBUG, INFO, WARN, ERROR)", "enum": ["DEBUG", "INFO", "WARN", "ERROR", "debug", "info", "warn", "error"], "default": "INFO" }, "format": { "type": "string", "description": "Console log output format", "enum": ["pretty", "json"], "default": "pretty" }, "fileFormat": { "type": "string", "description": "Log file output format. JSON (single line per entry) is recommended for production so logs are machine-parseable and shippable. Independent of the console 'format'. Global only: log files are a single shared stream, so this is not a per-server setting.", "enum": ["pretty", "json"], "default": "json" }, "redact": { "type": "array", "description": "Additional metadata key names to mask as [REDACTED] in all log output, on top of the always-on defaults. The defaults are matched as case-insensitive substrings: password, passwd, token, secret, apikey, api_key, authorization, credential, chatid (so encryptionPassword, botToken, etc. are covered).", "items": { "type": "string", "minLength": 1 } }, "actualApiVerbose": { "type": "boolean", "description": "UNSAFE. When true, the @actual-app/api library prints its own verbose output, which includes transactions (payees, amounts, notes), to the console. Default false: Actual-sync passes verbose:false to api.init. Global only.", "default": false }, "includeAccountNames": { "type": "boolean", "description": "When true, Actual account names appear in the logs. Default false: logs carry the account id only, and the name is omitted. Server names you choose are always logged. Notifications are not logs and keep account names. Global only.", "default": false }, "logDir": { "type": ["string", "null"], "description": "Directory for log files (set to null to disable file logging). For Docker deployments, use /app/logs. Defaults to './logs' if not specified.", "default": "./logs" }, "rotation": { "type": "object", "additionalProperties": false, "description": "Log rotation settings (enabled by default with 30-day retention)", "properties": { "enabled": { "type": "boolean", "description": "Enable log rotation (recommended for production)", "default": true }, "maxSize": { "type": "string", "description": "Maximum size per log file before rotation: a positive integer plus a unit B/K/M/G, optionally suffixed with B (e.g., '10M', '10MB', '512B', '1GB'). Rotation fires on whichever trigger comes first: this size or the interval.", "pattern": "^[1-9][0-9]*(B|[KMG]B?)$", "default": "10M" }, "interval": { "type": "string", "description": "Time-based rotation interval, as a positive integer plus a unit: s/m/h/d or M for months (e.g., '1d', '12h', '1M'). Note m = minutes, M = months. Ensures logs rotate even at low volume, so maxFiles behaves as documented (days of retention).", "pattern": "^[1-9][0-9]*[smhdM]$", "default": "1d" }, "maxFiles": { "type": "integer", "description": "Maximum number of rotated log files to keep. With the default '1d' interval this is days of retention.", "minimum": 1, "maximum": 1000, "default": 30 }, "compress": { "type": "string", "description": "Compression format for archived logs (gzip saves ~70% disk space)", "enum": ["gzip", "none"], "default": "gzip" } } }, "syslog": { "type": "object", "additionalProperties": false, "description": "Syslog centralized logging configuration", "properties": { "enabled": { "type": "boolean", "description": "Enable syslog output", "default": false }, "host": { "type": "string", "description": "Syslog server hostname", "minLength": 1, "default": "localhost" }, "port": { "type": "integer", "description": "Syslog server port", "minimum": 1, "maximum": 65535, "default": 514 }, "protocol": { "type": "string", "description": "Transport: udp (plaintext, not an audit log), tcp (plaintext, RFC 6587 framing) or tls (RFC 5425, certificate verified)", "enum": ["udp", "tcp", "tls"], "default": "udp" }, "ca": { "type": "string", "description": "Path to a PEM CA certificate to trust for protocol tls (added to the default trust store; verification is never disabled)" }, "facility": { "type": "integer", "description": "Syslog facility code (0-23)", "minimum": 0, "maximum": 23, "default": 16 } } }, "performance": { "type": "object", "additionalProperties": false, "description": "Performance logging settings", "properties": { "enabled": { "type": "boolean", "description": "Enable performance metrics logging", "default": false }, "thresholds": { "type": "object", "additionalProperties": false, "description": "Performance thresholds in milliseconds", "properties": { "slow": { "type": "integer", "description": "Threshold for slow operations (ms)", "minimum": 100, "maximum": 3600000, "default": 1000 }, "verySlow": { "type": "integer", "description": "Threshold for very slow operations (ms)", "minimum": 100, "maximum": 3600000, "default": 5000 } } } } } } }, "healthCheck": { "type": "object", "additionalProperties": false, "description": "Health check HTTP endpoint configuration", "properties": { "port": { "type": "integer", "description": "HTTP port for health check endpoints", "minimum": 1024, "maximum": 65535, "default": 3000 }, "host": { "type": "string", "description": "Host to bind health check server", "minLength": 1, "default": "0.0.0.0", "examples": ["0.0.0.0", "127.0.0.1", "localhost"] }, "trustProxy": { "description": "Trust X-Forwarded-For from a reverse proxy you control, so rate limiting and auth-failure logs use the real client IP. false (default) for direct exposure; a hop count (1 = one proxy in front), or an IP/CIDR string or array. true trusts every hop and lets any client spoof its IP if the service is reachable directly.", "default": false, "oneOf": [ { "type": "boolean" }, { "type": "integer", "minimum": 0, "maximum": 10 }, { "type": "string", "minLength": 1 }, { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } ], "examples": [false, 1, "172.18.0.2", ["loopback", "172.18.0.2"]] }, "hsts": { "type": "boolean", "description": "Send the Strict-Transport-Security header (max-age 180 days, no includeSubDomains). Off by default because the service is plain HTTP in most deployments; enable it only when browsers reach it over HTTPS (normally TLS and HSTS are the reverse proxy's job).", "default": false }, "auth": { "type": "object", "additionalProperties": false, "description": "Credential for GET /metrics and GET /metrics/prometheus (#247), separate from dashboard.auth so rotating one never breaks the other. Off by default. /health and /ready are never authenticated: they are liveness and readiness probes. With type basic or token the credential must be set, or startup fails.", "properties": { "type": { "type": "string", "description": "Authentication type for the metrics endpoints", "enum": ["none", "basic", "token"], "default": "none" }, "username": { "type": "string", "description": "Username for basic authentication (required if type=basic)" }, "password": { "type": "string", "description": "Password for basic authentication (required if type=basic)" }, "token": { "type": "string", "description": "Bearer token for token authentication (required if type=token). Prefer this over basic: a scrape credential should be a long random value, not a human password." } }, "allOf": [ { "if": { "required": ["type"], "properties": { "type": { "const": "basic" } } }, "then": { "required": ["username", "password"], "properties": { "username": { "type": "string", "minLength": 1 }, "password": { "type": "string", "minLength": 1 } } } }, { "if": { "required": ["type"], "properties": { "type": { "const": "token" } } }, "then": { "required": ["token"], "properties": { "token": { "type": "string", "minLength": 1 } } } } ] }, "dashboard": { "type": "object", "additionalProperties": false, "description": "Dashboard authentication and access control", "properties": { "enabled": { "type": "boolean", "description": "Enable web dashboard access", "default": true }, "allowedOrigins": { "type": "array", "description": "Extra browser origins (scheme://host[:port]) allowed to open the /ws/logs live log stream and to submit state-changing requests (POST and other writes) to the dashboard API. Same-origin is always allowed. Needed only when the dashboard is served under a different origin than the one the browser connects to, e.g. behind some reverse proxies. Review this list before enabling allowConfigWrites.", "items": { "type": "string", "pattern": "^https?://[^/\\s]+$" }, "default": [] }, "allowConfigWrites": { "type": "boolean", "description": "Allow the dashboard to change configuration (alert settings). Off by default; also refused while auth.type is none.", "default": false }, "auth": { "type": "object", "additionalProperties": false, "description": "Authentication configuration for dashboard", "properties": { "type": { "type": "string", "description": "Authentication type", "enum": ["none", "basic", "token"], "default": "none" }, "username": { "type": "string", "description": "Username for basic authentication (required if type=basic)" }, "password": { "type": "string", "description": "Password for basic authentication (required if type=basic)" }, "token": { "type": "string", "description": "Bearer token for token authentication (required if type=token)" } }, "allOf": [ { "if": { "required": ["type"], "properties": { "type": { "const": "basic" } } }, "then": { "required": ["username", "password"], "properties": { "username": { "type": "string", "minLength": 1 }, "password": { "type": "string", "minLength": 1 } } } }, { "if": { "required": ["type"], "properties": { "type": { "const": "token" } } }, "then": { "required": ["token"], "properties": { "token": { "type": "string", "minLength": 1 } } } } ] } } } } }, "syncHistory": { "type": "object", "additionalProperties": false, "description": "Sync history persistence configuration", "properties": { "dbPath": { "type": "string", "description": "Path to SQLite database file for sync history", "minLength": 1, "default": "data/sync-history.db" }, "retentionDays": { "type": "integer", "description": "Number of days to retain sync history", "minimum": 1, "maximum": 365, "default": 90 } } }, "prometheus": { "type": "object", "additionalProperties": false, "description": "Prometheus metrics export configuration", "properties": { "enabled": { "type": "boolean", "description": "Enable Prometheus metrics endpoint", "default": true }, "includeDefaultMetrics": { "type": "boolean", "description": "Include Node.js default metrics (memory, CPU, etc.)", "default": true } } }, "notifications": { "type": "object", "additionalProperties": false, "description": "Error notification configuration", "properties": { "notifyOnSuccess": { "type": "string", "description": "Global default for when to send sync notifications, overridable per channel. 'always' sends on every result; 'errors_only' sends on failure and partial but not success; 'activity_only' sends on failure and partial, and on success only when the sync imported or updated at least one transaction; 'never' turns the channel off entirely (failures included). Test notifications always send.", "enum": ["always", "errors_only", "activity_only", "never"], "default": "always" }, "email": { "type": "object", "additionalProperties": false, "description": "Email notification settings", "properties": { "notifyOnSuccess": { "type": "string", "description": "When to send sync notifications by email. Overrides notifications.notifyOnSuccess.", "enum": ["always", "errors_only", "activity_only", "never"] }, "enabled": { "type": "boolean", "description": "Enable email notifications", "default": false }, "host": { "type": "string", "description": "SMTP server host", "minLength": 1, "default": "smtp.gmail.com", "examples": ["smtp.gmail.com", "smtp.sendgrid.net"] }, "port": { "type": "integer", "description": "SMTP server port", "minimum": 1, "maximum": 65535, "default": 587 }, "secure": { "type": "boolean", "description": "Use TLS for SMTP connection", "default": false }, "auth": { "type": "object", "additionalProperties": false, "description": "SMTP authentication credentials", "properties": { "user": { "type": "string", "description": "SMTP username" }, "pass": { "type": "string", "description": "SMTP password or app-specific password" } } }, "from": { "type": "string", "description": "From email address. Validated as an email only when email notifications are enabled, so a disabled stub may hold a placeholder." }, "to": { "type": "array", "description": "Recipient email addresses (validated as emails only when email is enabled)", "items": { "type": "string", "minLength": 1 } } }, "if": { "required": ["enabled"], "properties": { "enabled": { "const": true } } }, "then": { "required": ["from", "to"], "properties": { "from": { "type": "string", "format": "email" }, "to": { "type": "array", "minItems": 1, "items": { "type": "string", "format": "email" } } } } }, "telegram": { "type": "object", "additionalProperties": false, "description": "Telegram bot settings for notifications and commands", "properties": { "enabled": { "type": "boolean", "description": "Enable Telegram bot", "default": false }, "botToken": { "type": "string", "description": "Telegram bot token from @BotFather. The 'digits:token' format is enforced only when telegram is enabled, so a disabled stub may hold a placeholder." }, "chatId": { "type": "string", "description": "Chat ID (user ID, group ID, or channel username)" }, "chatIds": { "type": "array", "description": "One or more chat IDs (alternative to chatId). The first entry is used for outbound notifications; the bot also accepts numeric IDs.", "items": { "type": ["string", "number"], "minLength": 1 } }, "notifyOnSuccess": { "type": "string", "description": "When to send sync notifications to Telegram. Overrides notifications.notifyOnSuccess. Also settable at runtime with the /notify bot command.", "enum": ["always", "errors_only", "activity_only", "never"] }, "pollInterval": { "type": "integer", "description": "Polling interval in milliseconds for the interactive bot", "minimum": 100, "maximum": 3600000, "default": 2000 } }, "if": { "required": ["enabled"], "properties": { "enabled": { "const": true } } }, "then": { "required": ["botToken"], "properties": { "botToken": { "type": "string", "pattern": "^[0-9]+:[A-Za-z0-9_-]+$" } }, "anyOf": [ { "required": ["chatId"], "properties": { "chatId": { "type": "string", "minLength": 1 } } }, { "required": ["chatIds"], "properties": { "chatIds": { "type": "array", "minItems": 1 } } } ] } }, "branding": { "type": "boolean", "description": "Stamp the Actual-sync project name and icon onto Slack/Discord/ntfy notifications. Set false to keep a webhook's own configured app identity, or to stop ntfy from fetching the project icon from GitHub on an air-gapped network.", "default": true }, "webhooks": { "type": "object", "additionalProperties": false, "description": "Webhook notification settings", "properties": { "telegram": { "type": "array", "description": "Legacy per-webhook Telegram bot configs (an alternative to the top-level notifications.telegram). Each entry posts to one bot + chat.", "items": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "description": "Webhook name for identification" }, "botToken": { "type": "string", "description": "Telegram bot token from @BotFather", "minLength": 1 }, "chatId": { "type": "string", "description": "Target chat ID", "minLength": 1 } }, "required": ["botToken", "chatId"] } }, "slack": { "type": "array", "description": "Slack webhook configurations", "items": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "description": "Webhook name for identification" }, "url": { "type": "string", "description": "Slack webhook URL", "pattern": "^https://hooks\\.slack\\.com/.*" }, "enabled": { "type": "boolean", "description": "Set false to disable this webhook without removing it", "default": true }, "notifyOnSuccess": { "type": "string", "description": "When to send sync notifications to this webhook. Overrides notifications.notifyOnSuccess.", "enum": ["always", "errors_only", "activity_only", "never"] } }, "required": ["url"] } }, "discord": { "type": "array", "description": "Discord webhook configurations", "items": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "description": "Webhook name for identification" }, "url": { "type": "string", "description": "Discord webhook URL", "pattern": "^https://discord\\.com/api/webhooks/.*" }, "enabled": { "type": "boolean", "description": "Set false to disable this webhook without removing it", "default": true }, "notifyOnSuccess": { "type": "string", "description": "When to send sync notifications to this webhook. Overrides notifications.notifyOnSuccess.", "enum": ["always", "errors_only", "activity_only", "never"] } }, "required": ["url"] } }, "generic": { "type": "array", "description": "Generic webhook configurations. Each receives a documented JSON payload (status, server, account counts and lists) via HTTP POST. Works with ntfy (JSON publishing), Gotify, Home Assistant, n8n, and custom endpoints.", "items": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string", "description": "Webhook name for identification" }, "url": { "type": "string", "description": "Destination URL the JSON payload is POSTed to", "minLength": 1, "pattern": "^https?://.+" }, "notifyOnSuccess": { "type": "string", "description": "When to send sync notifications to this webhook. Overrides notifications.notifyOnSuccess.", "enum": ["always", "errors_only", "activity_only", "never"] }, "enabled": { "type": "boolean", "description": "Set false to disable this webhook without removing it", "default": true }, "headers": { "type": "object", "description": "Extra HTTP headers to send (e.g. Authorization, Priority). Secrets are redacted in logs.", "additionalProperties": { "type": "string" } }, "contentType": { "type": "string", "description": "Override the request Content-Type", "default": "application/json" } }, "required": ["url"] } } } }, "ntfy": { "type": "object", "additionalProperties": false, "description": "ntfy push notifications (https://ntfy.sh). Posts to a topic URL with Title/Priority/Tags headers.", "properties": { "notifyOnSuccess": { "type": "string", "description": "When to send sync notifications to ntfy. Overrides notifications.notifyOnSuccess.", "enum": ["always", "errors_only", "activity_only", "never"] }, "enabled": { "type": "boolean", "description": "Enable ntfy notifications", "default": false }, "url": { "type": "string", "description": "Full topic URL, e.g. https://ntfy.sh/my-topic or https://ntfy.example.com/my-topic" }, "token": { "type": "string", "description": "Optional access token sent as 'Authorization: Bearer '. Redacted in logs." }, "priorityOnFailure": { "type": "string", "description": "ntfy priority for failure notifications", "enum": ["max", "urgent", "high", "default", "low", "min"], "default": "high" }, "priorityOnSuccess": { "type": "string", "description": "ntfy priority for success/info notifications", "enum": ["max", "urgent", "high", "default", "low", "min"], "default": "default" }, "tags": { "type": "array", "description": "Base ntfy tags/emoji added to every notification", "items": { "type": "string", "minLength": 1 } }, "icon": { "type": "string", "anyOf": [ { "const": "" }, { "format": "uri" } ], "description": "URL of an icon shown in the ntfy notification (sent as the Icon header). Defaults to the Actual-sync project icon; set to an empty string to send no icon." } }, "if": { "required": ["enabled"], "properties": { "enabled": { "const": true } } }, "then": { "required": ["url"], "properties": { "url": { "type": "string", "minLength": 1 } } } }, "thresholds": { "type": "object", "additionalProperties": false, "description": "Notification threshold settings", "properties": { "consecutiveFailures": { "type": "integer", "description": "Number of consecutive failures before notification", "minimum": 1, "maximum": 20, "default": 3 }, "failureRate": { "type": "number", "description": "Failure rate (0-1) threshold over rate period", "minimum": 0, "maximum": 1, "default": 0.5 }, "ratePeriodMinutes": { "type": "integer", "description": "Time period in minutes for failure rate calculation", "minimum": 5, "maximum": 1440, "default": 60 } } }, "rateLimit": { "type": "object", "additionalProperties": false, "description": "Notification rate limiting settings", "properties": { "minIntervalMinutes": { "type": "integer", "description": "Minimum minutes between notifications", "minimum": 1, "maximum": 1440, "default": 15 }, "maxPerHour": { "type": "integer", "description": "Maximum notifications per hour", "minimum": 1, "maximum": 20, "default": 4 } } } } } } }