Three ways into OpenPartner: server SDK, outbound webhooks, and partner postbacks
There's no single 'integration' surface for an attribution platform — brands, partners, and the platform itself all push and pull data on different schedules. Here's the three integration paths that just landed in OpenPartner, when each is the right one, and how they compose.
The most common question we get from new OpenPartner customers, after “how do I get started?”: “How does my stack talk to yours?”
It’s not a single answer. An attribution platform has at least three sets of conversations happening:
- The brand’s backend sending OpenPartner authoritative conversion events (“this user just paid invoice #X”).
- OpenPartner notifying the brand’s integrations when something happened on the ledger (“commission #Y just paid out, post it to Slack”).
- OpenPartner notifying the partner’s tracker when a conversion lands on their side (“here’s a Voluum postback for the click that just converted”).
Three audiences, three transport shapes, three different sets of trade-offs. We just shipped the third one — partner postbacks — which closes the loop. This post is a tour of all three so you can pick the right tool when you’re integrating.
Path 1: Server SDK (brand backend → OpenPartner)
This is the inbound path. Your application’s backend is the source of truth for what counts as a conversion — it knows when an invoice paid, when a trial converted, when a custom milestone fired. The server SDK is how you tell OpenPartner.
import { OpenPartnerServer } from '@openpartner/sdk/server';
const op = new OpenPartnerServer({ apiUrl: process.env.OPENPARTNER_API_URL!, apiKey: process.env.OPENPARTNER_SERVER_KEY!,});
// In your invoice.paid webhook handler from Stripe / Paddle / Lago / your own billing:await op.event('invoice_paid', { userId: customer.id, amount: 4900, // minor units — cents currency: 'USD',});When to use it: any conversion event you want to drive money. Especially invoice_paid —
keep that one server-authoritative; you don’t want a malicious browser extension faking
revenue events.
The SDK is a thin wrapper around POST /attribution/events with retry logic. If you’d rather
hit the HTTP API directly, that’s documented in Tracking events.
Path 2: Outbound webhooks (OpenPartner → brand integrations)
This is the brand’s notification stream. Subscribers register a URL + secret + event list under Admin → Webhooks, and OpenPartner POSTs a signed JSON payload every time something happens on the ledger.
The events surface is broad — every state change in the system fires:
attribution.created— a click + identity + event triple just attributedcommission.{accrued,approved,paid,reversed}— every commission state transitionpartner.{created,activated}— partner lifecyclepartnership.approved— a creator’s Network application got acceptedpayout.created— a payout batch went to Stripe Connect
Payloads are HMAC-signed (x-openpartner-signature header, same scheme Stripe uses), so
your receiver can verify the request actually came from us:
import { createHmac } from 'node:crypto';
function verify(req: Request, secret: string): boolean { const ts = req.headers['x-openpartner-timestamp']; const sig = req.headers['x-openpartner-signature']; const expected = createHmac('sha256', secret).update(`${ts}.${req.rawBody}`).digest('hex'); // Constant-time compare omitted for brevity. return sig === expected;}When to use it: any time you want the brand’s own infrastructure to react to OpenPartner state. Common shapes:
- A Slack message when a partner activates their account
- A row in the operations team’s data warehouse for every commission paid
- A Zapier / Make.com / n8n flow that pushes new partners into the brand’s CRM
- An internal alert when commission.reversed fires above a threshold
We ship official integration packages for Zapier and ActivePieces that consume this same surface; the underlying webhook system is what powers them.
Path 3: Partner postbacks (OpenPartner → partner’s tracker)
This is the new one. Path 2’s webhooks are tenant-scoped — they belong to the brand, and firing on every event across the whole program. That’s the right shape for the brand’s operations stack. It’s the wrong shape for a media-buyer creator who just wants conversions on their clicks delivered to their ad tracker.
That’s what postbacks are for. Each partner can register a URL on Postbacks in the partner sidebar, with macro placeholders that get substituted at fire time:
https://your-domain.voluumtrk.com/postback?cid={click_id}&payout={commission_amount}The partner doesn’t need to write code, run a webhook receiver, or sign anything. They paste the URL their tracker gave them, OpenPartner pings it on every commission event subscribed, and the tracker logs the conversion against the click ID it remembers from the click-out.
The macro list:
| Macro | Notes |
|---|---|
{click_id} | Originating click. Only on commission.accrued. |
{partner_id} | Partner ID. |
{commission_id} | Commission row. |
{commission_amount} | Decimal in major units. |
{currency} | 3-letter ISO. |
{event_id} / {transaction_id} | The Event row that drove the commission. Aliases. |
{event_type} | invoice_paid / signup / your custom type. |
{campaign_id} | Campaign / Program. |
{payout_id} | Only on commission.paid. |
Subscription is restricted to commission.* events — partner-state events stay on Path 2.
Unsigned by design. Affiliate trackers across the industry consume postbacks as
unauthenticated GETs. If you need verification, embed a shared secret in the URL template
yourself: ?secret=YOUR_SHARED_SECRET&cid={click_id} — your tracker checks it, OpenPartner
just substitutes the rest.
Audit lives in rolling counters on each postback row: Fired, Success, Failed,
Last fired, Last status. If Failed is climbing, something’s wrong on the tracker side
and the partner can fix it without engineering involvement.
When to use it: the partner runs paid traffic through Voluum, Bemob, RedTrack, BinomTracker, FunnelFlux, or any other ad tracker. They use that tracker to optimise spend, and they need the tracker to know about the conversion. Without postbacks, they’re flying blind on the part of the funnel that runs through OpenPartner.
If the partner is purely organic — Substack, YouTube, Twitter, a newsletter — they don’t need this and should skip it.
How they compose
The three paths layer cleanly because each one is upstream of the next:
Brand backend ──[server SDK]──▶ OpenPartner │ ├──[outbound webhook]──▶ Brand's Slack / CRM / DWH │ └──[partner postback]──▶ Partner's Voluum / RedTrackA commission.accrued event fires once and fans out:
- Tenant subscribers (Path 2) get a JSON
commission.accruedenvelope, signed. - Partner postbacks (Path 3) for that specific partner get a substituted GET.
Both fire-and-forget; both have independent retry / failure semantics; neither blocks the
inbound POST /attribution/events (Path 1) from returning to the brand’s backend.
Why three surfaces instead of one
You could argue we should pick one transport — say, signed JSON webhooks — and force every integration to consume that. We didn’t, for two reasons:
Different consumers expect different shapes. Affiliate trackers across the entire industry consume postbacks as URL-shaped GETs with macros. Asking a partner to “set up an HMAC-verified JSON receiver” instead of “paste this URL into Voluum’s postback field” is a “no, OpenPartner doesn’t support my tracker” answer disguised as engineering purity.
Authority models differ. Outbound webhooks are owned by the brand admin — they configure them, they hold the signing secret, they decide which events fire. Postbacks are owned by the partner — the brand doesn’t configure them, can’t see the URL, doesn’t hold credentials. That’s the right authority boundary, and stuffing both into one table breaks it.
The cost is a slightly larger surface area to document and explain (this post). The benefit is that whatever you’re integrating from — your billing backend, your ops Slack, your ad tracker — there’s a path shaped right for it.
What’s next
A few obvious extensions on the roadmap:
- Outbound webhook auto-retry with exponential backoff. Today retries are manual through the admin UI; the WebhookDelivery rows already capture every attempt and the scheduler exists, so this is mostly a “wire it up” task.
- Postback delivery log. Per-postback rolling counters get you “is this firing?” but not “show me the last 50 fires with timing and response bodies.” The infrastructure choices are subtle (a Tracker firing 1000+ times an hour for a high-volume partner shouldn’t blow up the database), so we’ve punted on it for v1 — the rolling counters are enough for the first wave of users.
Both should land in the next sprint or two. As ever, the GitHub issue tracker is the canonical roadmap — file a request if there’s a path your stack needs that we haven’t shipped yet.