Pardon
Contents

Hosted campaigns

No RMM? You can still reach every machine. Put the campaign JSON on a web server you control, and have a scheduled task on each PC run Pardon against that URL every hour or so. To send something new, you edit one file.

Two features make this work: --payload-url fetches the campaign, and show_once stops the same campaign popping up every hour.

Fetching the campaign

pardon.exe show --payload-url https://it.example.com/pardon/current.json

Pardon downloads the JSON and then handles it exactly like a file: the same checks, the same results, the same SYSTEM fan-out to signed-in users.

  • HTTPS only. Plain http:// is refused before any request is sent, except to localhost, 127.0.0.1 or ::1 for testing. Redirects are followed only to HTTPS.
  • It uses the machine's proxy settings and certificate store, as Windows itself would.
  • It waits up to 15 seconds and accepts files up to 256 KB, with no caching, so an edit takes effect on the next run.
  • If the download fails for any reason (refused URL, network error, timeout, an HTTP error status, a file too big), the run exits 10 with error code payload_fetch_failed and shows nothing.
  • --payload and --payload-url together is a usage error (exit 64).

Never put a secret in a hosted campaign. Anyone who can fetch the URL can read it. If you post results to a webhook, give each machine the secret through PARDON_WEBHOOK_SECRET instead; see Sending results to a webhook.

Showing it once

{
  "schema": "campaign/v1",
  "campaign_id": "vpn-upgrade-2026-11",
  "type": "dialog",
  "show_once": true,
  "title": "Your VPN is getting an upgrade",
  "body": "From Monday you'll approve sign-ins on your phone instead of typing a code.",
  "buttons": [ { "id": "ack", "label": "Got it", "style": "primary" } ],
  "branding": { "org_name": "Lakeside IT", "accent": "#1e3a5f" }
}

With "show_once": true, the first time anyone on a machine answers (acknowledges, submits, or dismisses), Pardon records it. Every later run on that machine shows nothing and reports suppressed, exit 7, suppressed_reason: "already_answered".

Only an answer counts. If the card timed out, nobody was signed in, or it was held back for presenting, nothing is recorded and the campaign can show again on the next run.

The record is per machine, in C:\ProgramData\Pardon\shown\<campaign_id>.json. Only SYSTEM and administrators can change that folder, so a user can't delete the record to be asked again, or create one to never be asked. To show a campaign again on one machine, run pardon.exe clear --campaign <id>. To show something new to everyone, give it a new campaign_id.

show_once is not allowed on reboot campaigns, which already remember each machine's answers in their own way (see Reboot campaigns).

Putting it together

  1. Copy pardon.exe to every machine (GPO, Intune, or your imaging).
  2. Register a scheduled task that runs as SYSTEM every hour: pardon.exe show --payload-url https://it.example.com/pardon/current.json. templates/scheduled-task (reference coming soon) has a script that does this.
  3. Publish a campaign with "show_once": true and a fresh campaign_id whenever you have something to say. Add expires_at so a laptop that was off for two weeks doesn't show old news.
  4. Collect answers with a webhook, since there is no RMM capturing the exit codes.

Reboot campaigns work this way too: their own deferral memory takes the place of show_once, so an hourly run just exits 7 until the user's deferral is up.