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:
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 clickEvery 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.source | Behavior |
|---|---|
"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):
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
- Recorded — a
conversionsrow with the resolved value, unique on(definitionId, eventId): re-evaluating the same event (Hatchet replay, concurrent ingest) can never double-fire. - Fanned out — one
conversion_dispatchesrow per destination, unique on(destinationId, event_id). - Delivered — a durable task loads the row, enriches it (contact
identifiers + the recovered click context: the most recent
campaign.arrivedat-or-before the conversion), and calls the destination. A failure retries with backoff; after five attempts the row is markedfailedwith 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.
Discord engagement alerts in a channel
Page a Discord channel on hand-raises, spam complaints, and journey completions with a filtered defineDestination() on the durable outbound spine — Discord-markdown content, an incoming-webhook body, and 204-as-success, with no per-journey Discord code.
Impact: attribution in your first week
What you know on day 1, day 3, and day 7 — arrivals, influenced, credited revenue per journey, and your first lift read. The checklist, and exactly what each number does and does not claim.