AlertHub + Hookdeck: Polar product alerts without a server

AlertHub + Hookdeck: Polar product alerts without a server

Polar, Stripe, and GitHub speak their own JSON. AlertHub speaks ah_title, ah_level, and ah_key. Hookdeck Event Gateway sits in the middle: it acknowledges the provider instantly, verifies the signature, optionally rewrites the payload, and POSTs a clean alert to AlertHub. You do not run a server.

They are not alternatives. Hookdeck is webhook plumbing (queue, HMAC, retries, inspect/replay). AlertHub is the human layer: a searchable dashboard plus Slack, Telegram, ntfy, Gotify, email, Discord, Teams. Change chat tools in Connections; Polar keeps talking to Hookdeck.

Polar  →  Hookdeck (verify → filter → transform)  →  AlertHub ingest  →  dashboard + channels

Hookdeck’s free Developer plan includes 10,000 events/month. That is more than enough for AlertHub Pro (1,000 alerts/month).

Why put Hookdeck in front of AlertHub

Pointing Polar straight at https://api.thealerthub.com/sendAlert works if you stuff placeholders into the query string — same trick as Stripe and GitHub. Three things break down as soon as the payload is rich:

GapNative URL to AlertHubWith Hookdeck
HMAC / Standard Webhooksingest does not verify Polar’s signatureSource verifies webhook-id / webhook-timestamp / webhook-signature
Archive vs renamePolar has no product.archived event — archive is product.updated + data.is_archivedTransformation sets critical vs warning
Secret in Polar’s dashboardah_key in the endpoint URLah_key lives in a Hookdeck transformation variable
Polar is down / you are debuggingPolar retries blindlyQueue, Issues, bulk replay

Use Hookdeck when the source is a SaaS webhook (Polar, Stripe, Shopify). Skip it for cron, bash, n8n, or the Ingestion API you already control.

How the pieces connect

  1. Polar POSTs its native event to a Hookdeck Source URL. Polar gets 200 immediately.
  2. A Filter (optional) drops everything that is not product.updated.
  3. A Transformation maps the Polar product to AlertHub fields and injects ah_key from process.env.
  4. The Destination is POST https://api.thealerthub.com/sendAlert.
  5. AlertHub authenticates the project, stores the row, and handle-new-alert fans out. Only warning and critical leave the dashboard; info is history only. See alert levels.

Rule order: Filter then Transform. Hookdeck runs the filter after the transform by default. Drag Filter above Transform, or the filter will look for body.type on a body that already only has ah_*.

Setup (once)

1. AlertHub project

In the app, open the project and copy the secret key (ah_key). Connect Slack, Telegram, ntfy, or Gotify under Connections so the first real event has somewhere to go.

2. Hookdeck Source + Destination

In the Hookdeck dashboard:

  1. Create a Source. Name it polar. Copy the Source URL (https://hkdk.events/…).
  2. Create a Destination. URL https://api.thealerthub.com/sendAlert, method POST. Do not put ah_key in this URL.
  3. Create a Connection from that Source to that Destination.

Authentication on the Source: Polar signs with Standard Webhooks. In Polar you can set your own secret when creating the endpoint — generate one, paste it in Polar and in the Hookdeck Source verification field. If Polar generates the secret after save, copy it back into Hookdeck before you rely on production traffic.

3. Polar webhook

In Polar: Settings → Webhooks → Add endpoint.

  • URL: the Hookdeck Source URL (not AlertHub).
  • Format: Raw.
  • Events: product.updated (add product.created if you also want new catalog items). Polar does not emit product.archived.

4. Filter (optional)

On the connection, add a Filter rule above Transform:

{
  "type": "product.updated"
}

Only archives (skip name/price edits):

{
  "type": "product.updated",
  "data": { "is_archived": true }
}

Retries toward AlertHub: retry 5xx and timeouts. Do not retry 401 (bad ah_key) or 400 (missing title). A 429 is the monthly AlertHub quota — hammering ingest will not help until the next cycle.

Store ah_key as a Hookdeck variable

Do not paste the AlertHub project key into Polar, into the Destination URL, or into the transformation source as a string literal. Hookdeck transformations support environment variables for secrets.

  1. Open the connection → Transform → Create new transformation (or Editor on an existing one).
  2. In the transformation editor, open the Variables dropdown (top right of the code pane).
  3. Add a variable named AH_KEY. Paste the AlertHub secret key as the value. Save.
  4. In the JavaScript, read it with process.env.AH_KEY — never interpolate it into request.query or Polar’s endpoint URL.

The variable is scoped to that transformation. Anyone who can edit the transformation can see it; Polar’s webhook UI cannot. If you rotate the AlertHub key in the project, update only AH_KEY in Hookdeck — Polar’s endpoint URL stays the same.

Test payloads in the editor’s Input tab do not automatically inject env vars into a screenshot you share. After Run, confirm Output contains ah_key and that you are not committing the Output tab to git.

Copy-paste: Polar product.updated → AlertHub

Paste this as the transformation handler. It treats an archived product as critical and any other update as warning.

addHandler("transform", (request) => {
  const event =
    typeof request.body === "string" ? JSON.parse(request.body) : request.body;
  const product = event.data || {};
  const archived = Boolean(product.is_archived);

  const price = (product.prices && product.prices[0]) || {};
  const priceLabel =
    price.amount_type === "free"
      ? "free"
      : price.price_amount != null
        ? `${(price.price_amount / 100).toFixed(2)} ${(price.price_currency || "").toUpperCase()}`
        : "n/a";

  request.body = {
    ah_key: process.env.AH_KEY,
    ah_source: "Polar",
    ah_level: archived ? "critical" : "warning",
    ah_title: archived
      ? `Polar: product archived — ${product.name || product.id}`
      : `Polar: product updated — ${product.name || product.id}`,
    ah_description: archived
      ? `${product.name} (${product.id}) is no longer for sale. Existing subscriptions keep their benefits.`
      : `ID: ${product.id}\nVisibility: ${product.visibility}\nRecurring: ${product.is_recurring} (${product.recurring_interval || "one-time"})\nPrice: ${priceLabel}`,
  };

  request.headers["content-type"] = "application/json";
  delete request.headers["webhook-signature"];
  delete request.headers["webhook-id"];
  delete request.headers["webhook-timestamp"];

  return request;
});

Drop Polar’s signature headers after rewriting the body: they no longer match, and AlertHub does not verify them.

Sample input (paste into Hookdeck Input → Run)

{
  "type": "product.updated",
  "timestamp": "2026-09-01T10:12:00.000Z",
  "data": {
    "id": "prod_01ABC",
    "name": "Pro monthly",
    "is_archived": false,
    "is_recurring": true,
    "recurring_interval": "month",
    "visibility": "public",
    "prices": [
      {
        "amount_type": "fixed",
        "price_amount": 1000,
        "price_currency": "eur"
      }
    ]
  }
}

Expected output body: ah_source: Polar, ah_level: warning, title containing Pro monthly, ah_key from the variable. Flip is_archived to true and Run again — level should become critical.

Name the transformation (e.g. polar-product-to-alerthub), Confirm, then Save the connection.

What you should see

Archive or rename a product in Polar (sandbox is enough). Hookdeck Requests shows the inbound Polar JSON; Events shows the transformed POST. AlertHub’s live feed gets the row. Connected channels receive warning / critical only.

One Hookdeck Source can fan out to several Destinations. Keep a second connection without this transform if another system still needs Polar’s original signed body.

Ideas to squeeze more value

  • Same pattern for Stripe / GitHub / Shopify. New Source per provider, same AlertHub Destination, a transform that maps type → ah_title / ah_level. Native placeholder URLs stay fine for trivial events — webhook builder.
  • subscription.canceled at warning, order.paid at info. Info never pages; it still sits in the dashboard.
  • ntfy or Gotify for on-call, Slack for the team. Add both in Connections — hybrid on-call. Polar does not need a Discord/Slack format; leave Polar on Raw.
  • Hookdeck Issues → AlertHub. When Hookdeck cannot deliver to ingest, that Issue can be a second connection that pages you. Meta-alerting for the plumbing.

FAQ

Does Polar POST to Slack?
No. Polar → Hookdeck → AlertHub → your Connections.

Can I skip the transformation?
Yes: Destination URL with query params (ah_key, ah_title={{type}}) like the Stripe guide. You lose archive-vs-update levels and you put ah_key in a URL. Fine for a smoke test, not for catalog changes.

Will Hookdeck retry AlertHub 429s all month?
Configure 4xx as non-retryable except maybe a short delay on 429 if you expect a quota bump. Do not use the default “retry for a week” on 401.

Does this replace Polar’s own retry log?
Hookdeck ACKs Polar quickly, so Polar’s retries should stay quiet. Debugging happens in Hookdeck (replay) and in AlertHub (history).

Next steps

Create the Source, paste its URL in Polar for product.updated, add AH_KEY, paste the transformation, send a test from Polar sandbox.

Related: Ingestion API · Stripe webhooks · GitHub webhooks · ntfy destination