Skip to content

Measuring Lifecycle Emails With Koltrix Delivery Events

How to use Koltrix webhooks and message events to measure onboarding, trial and dunning emails by what users do next, not by open rates you can't trust.

Koltrix Team5 min read
Performance analytics charts on a laptop screen
Photo by Luke Chesser on Unsplash
On this page(7 sections)
  1. What Koltrix records for each message
  2. The events worth subscribing to
  3. Step 1: tag each send with its lifecycle step
  4. Step 2: receive and verify events
  5. Step 3: measure outcomes, not opens
  6. Why opens deserve suspicion
  7. When to turn tracking off
  8. A minimal reporting table
  9. Key takeaways

Sending lifecycle email is the easy half. Knowing whether your day-3 onboarding nudge actually moves anyone, or whether your dunning sequence recovers more than it annoys, means joining what happened to the email with what the user did afterwards.

Koltrix gives you the email half of that join: a message ID on every send, a small set of signed webhook events, and a per-message status you can query. This post shows how to wire those into your own database so the question "did this email work?" has an answer.

What Koltrix records for each message

Every send through POST /api/v2/emails returns an id straight away. The message's status is deliberately simple, with only three values:

queued → sent      (the receiving server accepted it)
       ↘ bounced   (hard bounce, recipient auto-suppressed)

Opens and clicks are not statuses. They arrive as events, which you record yourself. You can follow a message in two ways:

  • Polling. GET /api/v2/messages/{id} returns the message's current status: queued, sent or bounced.
  • Webhooks. Koltrix posts events to an HTTPS endpoint you control as they happen, so you don't have to poll at all.

For lifecycle measurement, webhooks are usually the better fit, as long as you design for the delivery model described in step 2: you store each event next to your own data the moment it arrives.

The events worth subscribing to

Koltrix sends four webhook events, and that's the whole list:

Event When it fires Useful for
message.sent The receiving server accepted the message Confirming the email actually left
message.bounced Hard bounce; the recipient is auto-suppressed Cleaning user records, spotting bad signups
message.opened The tracking pixel loaded A rough signal, with caveats below
message.clicked A tracked link was clicked Intent to act, and which link

There is no separate "delivered", "complained" or "unsubscribed" event. "Sent" means a receiving server accepted the message, which is as close to delivery as SMTP can tell anyone; whether it then landed in the inbox or the spam folder isn't visible to any sender. Complaints and unsubscribes still matter, but you measure them from your own side, as step 3 shows.

Each delivery is a POST with a JSON body that includes the event, a timestamp, the message_id, to, from and subject, plus event-specific fields such as the url on a click or the bounce_reason on a bounce.

Step 1: tag each send with its lifecycle step

Koltrix tells you what happened to message 01HX9C8.... Only your application knows that this was the day-3 onboarding email for user 4812. So the first job is to record that mapping at send time:

const res = await fetch("https://api.koltrix.com/api/v2/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `onboarding-day3-${user.id}`,
  },
  body: JSON.stringify({
    from: "[email protected]",
    to: [user.email],
    subject: "Your workspace is set up. Here's the next step",
    body_html: html,
    body_text: text,
  }),
});
const { id } = await res.json();

await db.query(
  `insert into lifecycle_sends (message_id, user_id, email_key, sent_at)
   values ($1, $2, $3, now())`,
  [id, user.id, "onboarding_day3"],
);

The Idempotency-Key matters here too. Koltrix caches the first response for 24 hours per key, so a worker that crashes and retries gets the original response back instead of sending the same onboarding email twice.

Step 2: receive and verify events

Every webhook payload is signed with HMAC-SHA256 using a per-endpoint secret, sent in the X-Koltrix-Signature header as sha256= plus the hex digest of the raw request body. Verify it before storing anything:

import crypto from "node:crypto";

app.post("/hooks/koltrix", express.raw({ type: "application/json" }), async (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.KOLTRIX_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");
  const got = req.get("X-Koltrix-Signature") ?? "";
  if (expected.length !== got.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // acknowledge fast, process after
  const evt = JSON.parse(req.body.toString());
  await db.query(
    `insert into email_events (message_id, event, occurred_at)
     values ($1, $2, to_timestamp($3))
     on conflict (message_id, event, occurred_at) do nothing`,
    [evt.message_id, evt.event, evt.timestamp],
  );
});

Three details from the webhook docs are worth building in from day one:

  • Use the raw body. If your framework parses JSON first, the signature won't match.
  • Respond quickly, and keep your endpoint up. Each event is attempted once, with an 8-second timeout. There are no retries, so an event that hits a deploy, an outage or a slow handler is simply missed. Acknowledge first and process afterwards, as the handler above does.
  • Treat events as a signal, not a ledger. Because a missed event isn't redelivered, don't build billing or anything critical on webhooks alone. For a message you care about, poll its status. Keeping the insert idempotent, as the on conflict clause does, is still cheap insurance.

Step 3: measure outcomes, not opens

With sends and events in your database, the useful questions become simple joins against your own product data.

Did the onboarding email cause activation? Compare the activation rate of users who received onboarding_day3 with a small holdout group who didn't. message.sent and message.bounced tell you who it actually reached.

Is the trial-ending email converting? For each trial_ending send, check whether the workspace upgraded within a few days of it being sent. Clicks on the upgrade link are a useful leading indicator.

Is the dunning sequence recovering revenue? Join dunning_* sends to payment-retry results. Clicks on the update-card link show which email in the sequence does the work.

Is any email unwelcome? Koltrix doesn't send complaint or unsubscribe events, so build this signal from your own data. Record unsubscribes from your own preference page against the email key that linked to it. Count replies that land in your inbox asking you to stop. Watch bounces per email key, and watch for an email whose sends keep going out while clicks and product outcomes stay flat. One email that consistently underperforms the rest of the sequence on all of those is worth rewriting or cutting.

Why opens deserve suspicion

Opens come from a tracking pixel. Some mail clients and privacy features load images automatically, which registers an open whether or not a person read anything. Even a clean open count tells you about attention, not outcomes. Use opens to spot broken emails, such as a message that suddenly gets almost none, not to decide which email works.

When to turn tracking off

Open and click tracking are on by default. For mail where tracking adds nothing and rewritten links could look odd, like password resets and receipts, the docs describe opting out per message on the SMTP relay with the X-Koltrix-No-Track: 1 header.

A minimal reporting table

Once events flow in, one query per week covers most lifecycle questions:

Email key Sent Bounced Clicked Unsubscribed (your data) Outcome rate
welcome % activated within 7 days
onboarding_day3 % activated vs holdout
trial_ending % upgraded within 3 days
dunning_1 % payments recovered

The "outcome rate" column is the one that matters, and it's the one only your own data can fill in.

Key takeaways

  • Store the Koltrix message id next to your user and lifecycle step at send time. That mapping is what makes every later question answerable.
  • Koltrix sends four events: sent, bounced, opened and clicked. Each is attempted once with no retries, so verify the signature, acknowledge fast and poll status for anything critical.
  • Judge lifecycle emails by product outcomes, clicks and your own unsubscribe data, not by open rates.
  • Use Idempotency-Key on sends so retries never produce duplicate emails.

Start with Koltrix

Your domain, one inbox, and an API that sends.

A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

SharePost on XLinkedIn