Skip to content

The relationship model

Boomin is a programmable relationship engine, and distribution is what it runs over those relationships. Everything in the Platform API is one of six nouns, plus an Operation that carries async work.

The first three are the relationship stack — two peers and the durable bond between them:

YOUR BRAND ─────────── Relationship ─────────── Entity
(rel_...) (ent_...)
what the bond carries │ what each peer contributes
──────────────────── │ ─────────────────────────
Enrollment (enr_...) — participation in ONE program
· operating type — the CAPACITY the entity acts in (otype_...)
· requirement — negotiated per-member terms (ovr_...)
overrides
Assertions (asrt_...) — facts YOUR system states about the entity,
without handing Boomin the underlying data

Both peers stay thin; the relationship is where the richness lives — terms, capacity, negotiated policy, and the tenant truth your backend asserts.

Entity a durable identity you have relationships with
Relationship the durable brand ↔ entity bond (program-independent)
Enrollment that relationship's participation in ONE program
Distribution intent — a coordinated objective. Never an execution mode.
Deployment execution — one concrete thing running somewhere
Performance measurement, recorded against the deployment

A Distribution is Boomin’s PaymentIntent: you declare the objective, Boomin fans it out into Deployments and reports back what actually happened.

A durable identity you have relationships with: a creator, an affiliate, an advisor, an agency, another company. Entities exist independently of any one program, and you never create them directly — inviting someone by email upserts the identity for you.

Read-only in the API: boomin.entities.list(), boomin.entities.retrieve(id). (Entity is the legacy name; boomin.entities delegates here forever.)

The durable bond between one brand and one entity. Exactly one relationship exists per (brand, entity) pair, and it outlives any individual program. (Relationship is the legacy name; aliased forever.)

status: pendingactive → (paused) → ended.

  • Created pending at the first invite.
  • Flips to active when the first enrollment is approved.
  • pause() pauses that entity’s promo links across every program — never the shared deployment channel. Enrollments and connection grants are preserved untouched, and the paused links keep resolving, so attribution continues.
  • end() is the explicit terminal command. It never fires automatically.

Three primitives make the relationship programmable rather than a flat row:

  • Assertions — tenant truth. Your backend computes a private condition (verification, membership, KYC) and asserts only the outcome; requirements gate standing on the claim via assert:<key>, and revocation or expiry de-qualifies. Boomin never sees the underlying data.
  • Operating types — capacity. Your vocabulary for how an entity participates (advisor, reseller, agency); typed requirements and money rules apply only to enrollments operating in that capacity.
  • Requirement overrides — negotiated terms. Patch, suppress, or add requirements for one enrollment; effective policy is program ∘ operating type ∘ enrollment.

The fact vocabulary these evaluate over is extensible too: metric keys let a brand register its own x:-namespaced metrics by API call — with each surface admitting exactly the vocabulary its rail can execute.

A relationship’s participation in one program. This is where the referral code lives (referralCode is unique per program by design).

Enrollments carry two orthogonal fields — this trips people up, so be precise:

FieldValuesMoved by
approvalStatuspending approved rejectedapprove() / reject() only
statusactive paused archivedpause() / resume() only

Approval commands never touch status. Pause/resume never touch approvalStatus. Rejection is not terminal — re-inviting a rejected enrollment resets approval to pending, and you may approve a previously rejected enrollment directly.

resume is the canonical verb on every surface. There is no unpause.

Intent, and only intent. A distribution says what business outcome you want and which programs supply eligible entities; it never says “post to Instagram”. It carries:

  • an objective (open text; the suggested set is awareness, acquisition, launch, conversion, retention, event_promotion, custom),
  • programs — the programs whose approved enrollments are eligible,
  • an optional budget (none | metered | funded),
  • a spec — the deployment plan,
  • optional subjects — descriptive context (an event, offer, or resource) that never constrains execution.

There is no kind column and no program column. A distribution is not a campaign, not a channel, and not a post.

Execution truth. Launching a distribution fans it out into one deployment per (program × planned slot) — a channel, never a person — each with a stable deploymentKey like program_<id>:boomin:referral_link:primary. The adapter mints one promo link per approved entity beneath each entity-program channel; per-entity attribution rides on each performance event’s enrollment.

A deployment separates what you asked for from what the world reports back:

  • desiredstatus: active | paused | canceled
  • observedobservedStatus: pending | provisioning | live | paused | pending_review | rejected | failed | completed | unknown

Measurement, always recorded against a deployment. Ingest business events with boomin.performance.events.create(...); read rollups with boomin.performance.summary(...).

Every external mutation returns an Operation — launch, pause, resume, cancel. Operations are the progress surface; they are never “the response you were waiting for”. Statuses: pending, running, waiting, succeeded, partial, failed, canceled.

operations:read is granted to every valid token implicitly, so any key can poll the operations it caused.

This is the part worth reading twice. A brand can run entity distribution on either of two rails, and they compose.

Rail 1 — an evergreen program, no distribution

Section titled “Rail 1 — an evergreen program, no distribution”

The classic referral/affiliate rail. It needs no distribution at all.

  1. Create a program, invite entities (enrollments.create), approve them.
  2. Each approved enrollment gets a program referralCode.
  3. Qualification requirements and tiers evaluate continuously from tracked activity.
  4. Rewards and payouts accrue against the program.

Nothing is launched. Nothing is budgeted. The program simply runs, and it keeps running until you pause or archive the enrollment. Activity on this rail is written as program metric events and feeds qualification and rewards.

const enrollment = await boomin.enrollments.create({
program: "prog_...",
});
await boomin.enrollments.approve(enrollment.id);
// enrollment.referralCode is live. That is the whole rail.

A distribution is a time-boxed, funded, measurable push that uses the same entities.

  1. Create a distribution referencing one or more programs.
  2. Validate, then launch.
  3. Boomin creates one deployment per (program × slot) — a shared channel whose adapter mints one promo link per approved entity, distinct from the program’s evergreen referral code.
  4. Conversions route by deployment and carry their own enrollment, so two distributions sharing the same program credit separately — each has its own channel and links.
  5. A funded budget draws down as rewards are granted, and the unconsumed remainder is released when the distribution is canceled or completed.
const distribution = await boomin.distributions.create({
name: "Spring launch",
objective: "launch",
programs: ["prog_..."],
budget: { mode: "funded", asset: "credit", total: 100000 },
});
await boomin.distributions.validate(distribution.id);
const { operation } = await boomin.distributions.launch(distribution.id);
Evergreen programDistribution
LifecycleRuns until paused/archiveddraftreadylaunchingactivecompleted
AttributionOne program referral code per enrollmentOne promo link per entity per deployment
BudgetNone (rewards accrue)Optional metered or funded reservation
MeasurementProgram metric events → qualification, rewardsPerformance events → deployment/distribution rollups
PausingPause an enrollmentPause the distribution or one deployment
Good forAlways-on affiliate/ambassador programsLaunches, drops, events, seasonal pushes

The rails are not exclusive. The evergreen program is the durable relationship layer; distributions are what you run through it. Program activity that happens outside any distribution stays on the program rail permanently — it is not migrated, and it keeps feeding qualification.

Pausing has deliberately asymmetric consequences, and they are resolved at event time (when the activity occurred), not at ingestion time:

While pausedLinks resolveAttributionRewardsBilling
Enrollment pausedyescontinuesstopscontinues
Relationship pausedyescontinuesstopscontinues
Enrollment archivedcode retained for historystopsstops
Relationship endedstopsstops

Pausing never dodges the active-entity fee while the entity’s links still resolve. archive and relationship.end are the billing exits. See Pricing for what “active entity” means.

Instagram, TikTok, Meta Ads and the rest are internal adapters. They never appear in the public API as a resource, a parameter, or a route. You express intent (mode / medium / channel / format in the deployment plan) and the registry resolves an adapter — or rejects the combination up front with channel_type_not_yet_supported during validate().

Today exactly one adapter is registered: the Boomin relationship adapter, which supports program / referral / boomin / referral_link. Everything else fails validation rather than failing at launch.