Hogsend is brand new.Try it
Hogsend
Conversions, Pixels & Ad Platforms

Conversions & Ad-Platform Feedback

Declare conversion points in code, resolve their value, and feed them back to ad platforms server-side — first-party click capture, durable dispatch, deterministic dedup.

The short version

A conversion point is a code-first declaration that a specific event — under specific conditions — counts as a valued conversion:

src/conversions/index.ts
import { defineConversion } from "@hogsend/engine";

export const dealSold = defineConversion({
  id: "deal-sold",
  name: "Deal sold",
  trigger: { event: "deal.sold" },
  destinations: ["meta-capi"],
});

export const conversions = [dealSold];

When the event fires, the engine records the conversion durably, resolves its value, recovers the contact's ad-click evidence (the fbclid and the real click timestamp captured by @hogsend/js weeks earlier), and dispatches it to each destination through a retrying durable task with a deterministic event_id — so the platform counts it exactly once, no matter how many retries it takes.

The reference destination is @hogsend/plugin-meta-capi (Meta Conversions API). Platforms without a native destination yet are covered by the PostHog CDP path below.

Why feed conversions back at all

Ad platforms optimize toward whatever signal you send them. Send clicks and they find clickers; send form-fills and they find form-fillers. Send sold deals with values and they bid toward people who resemble your actual buyers. That feedback loop is the difference between spraying budget and compounding it — and it only works if the conversion arrives server-side with good identity (hashed email + the original click ID) and honest values.

Earlier versions of these docs recommended forwarding conversions through PostHog's Destinations pipeline instead of shipping a native integration. That guidance is superseded: Hogsend now captures click IDs first-party (campaign.arrived via @hogsend/js), stitches them to the contact at lead intake, and owns the conversion record — so the engine has strictly better identity evidence than a mirror of it, and the loop no longer depends on a third party keeping a feature alive. The PostHog path still works and remains the recommended route for platforms without a native destination.

The loop, end to end

Ad click            @hogsend/js fires campaign.arrived (fbclid + utm_*, real timestamp)

Lead form           lead.submitted via any vendor webhook — hidden fields stitch
                    the browser session to the email-anchored contact

CRM stages          deal.quoted / deal.sold money events (deals ledger)

Conversion fires    defineConversion matches the event → conversions row (durable)

Dispatch            per-destination rows → durable task → Meta CAPI, retried with
                    the SAME event_id; fbc reconstructed from the stored click

Every hop is on one contact timeline, so the value that comes out the bottom is attributable to the click that went in the top.

defineConversion

defineConversion({
  id: "high-value-quote",                    // stable — recorded on every fired row
  name: "Quote over £10k",
  trigger: {
    event: "deal.quoted",
    where: (b) => b.prop("value").gte(10000), // same condition builder journeys use
  },
  value: { source: "event" },                // where the money comes from (default)
  sources: ["crm"],                          // ingest-source allowlist (see below)
  destinations: ["meta-capi"],               // conversion-destination ids
  attributionWindowDays: 90,                 // lookback for attribution credit
});

Value resolution — three sources:

value.sourceBehavior
"event" (default)The event's first-class value/currency (the revenue spine)
"fixed"A constant per conversion — { source: "fixed", amount: 50, currency: "GBP" } (e.g. a known LTV proxy for a booked call)
"property"Read from event properties — { source: "property", key: "total", currencyKey: "ccy" }

A conversion may legitimately resolve no value (a booked-call signal); destinations then send it as a lead-type event rather than a purchase.

The forged-value guard — browser events are pk_-trust-tier: anyone with your publishable key can mint any event with any value. By default a conversion point accepts every ingest source except inapp (browser). Narrow it further with an explicit allowlist (sources: ["crm"]) or — only for value-less signals you'd accept from anyone — open it with sources: "any".

Wiring

Conversion points and destinations are content, registered on the client in both entry points (HTTP + worker):

src/index.ts / src/worker.ts
import { createMetaCapiDestination } from "@hogsend/plugin-meta-capi";
import { conversions } from "./conversions/index.js";

const client = createHogsendClient({
  journeys,
  conversions,
  conversionDestinations: [
    createMetaCapiDestination({
      pixelId: process.env.META_PIXEL_ID!,
      accessToken: process.env.META_CAPI_TOKEN!,
    }),
  ],
});

What happens when one fires

  1. Recorded — a conversions row with the resolved value, unique on (definitionId, eventId): re-evaluating the same event (Hatchet replay, concurrent ingest) can never double-fire.
  2. Fanned out — one conversion_dispatches row per destination, unique on (destinationId, event_id).
  3. Delivered — a durable task loads the row, enriches it (contact identifiers + the recovered click context: the most recent campaign.arrived at-or-before the conversion), and calls the destination. A failure retries with backoff; after five attempts the row is marked failed with the error, never lost.

Deduplication is deterministic, not best-effort. The event_id is sha256(contactId : definitionId : eventRowId) — stable across retries and re-evaluations. If you also run the platform's browser pixel, have it send the same id and the platform reconciles the pair (Meta dedups on event_name + event_id within ~48h).

Authoring a destination

A destination is a small provider — meta + one send:

import { defineConversionDestination } from "@hogsend/engine";

const myDestination = defineConversionDestination({
  meta: { id: "my-platform", name: "My Platform" },
  async send(input) {
    // input: eventId, definitionId, triggerEvent, value, currency,
    // occurredAt, contact { email?, phone?, externalId?, anonymousId? },
    // clicks { clickIds, clickAt?, landingPage? }
    // Throw to retry; return { response } to record the platform's receipt.
  },
});

Contact identifiers arrive unhashed — hashing is platform-specific, so each destination owns its own normalization (plugin-meta-capi SHA-256s email/phone per Meta's spec and reconstructs fbc from the stored click).

The PostHog CDP alternative

For platforms Hogsend has no native destination for yet, PostHog's Destinations pipeline forwards conversion events server-side to Google Ads, TikTok, LinkedIn, and Reddit — with the hashing and click-id mapping maintained by PostHog. Hogsend's analytics mirror captures your events (with value/currency) into PostHog, so the event stream those destinations need is already there.